find-binary-version 0.5.0

Identify binary versions easily
Documentation
// Copyright (C) 2019-2021 O.S. Systems Sofware LTDA
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::VersionFinder;
use regex::bytes::Regex;
use std::{io::SeekFrom, str};
use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};

#[allow(clippy::enum_variant_names, clippy::upper_case_acronyms)]
enum LinuxKernelKind {
    ARMzImage,
    UImage,
    X86bzImage,
    X86zImage,
}

// U-Boot Image Magic Number
const UIMAGE_MAGIC_NUMBER: u32 = 0x2705_1956;

// zImage Magic Number used in ARM
const ARM_ZIMAGE_MAGIC_NUMBER: u32 = 0x016F_2818;

async fn discover_linux_kernel_kind<R: AsyncRead + AsyncSeek + Unpin>(
    buf: &mut R,
) -> Option<LinuxKernelKind> {
    // U-Boot Image Magic header is stored at begin of file
    buf.seek(SeekFrom::Start(0x0000)).await.ok()?;
    if buf.read_u32().await.ok()? == UIMAGE_MAGIC_NUMBER {
        return Some(LinuxKernelKind::UImage);
    }

    // ARM zImage Magic header is stored at offset 0x0024 of file
    buf.seek(SeekFrom::Start(0x0024)).await.ok()?;
    if buf.read_u32_le().await.ok()? == ARM_ZIMAGE_MAGIC_NUMBER {
        return Some(LinuxKernelKind::ARMzImage);
    }

    // Taken from: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/x86/boot.txt#n144
    //
    // Offset  Proto   Name            Meaning
    // /Size
    // ...
    // 01FE/2  ALL     boot_flag       0xAA55 magic number
    // ...
    // 0211/1	2.00+	loadflags	Boot protocol option flags

    // Verify the boot_flag magic number
    buf.seek(SeekFrom::Start(0x01FE)).await.ok()?;
    if buf.read_u16_le().await.ok()? != 0xAA55 {
        return None;
    }

    // Field name:	loadflags
    // Type:		modify (obligatory)
    // Offset/size:	0x211/1
    // Protocol:	2.00+
    //
    //   This field is a bitmask.
    //
    //   Bit 0 (read):	LOADED_HIGH
    //         - If 0, the protected-mode code is loaded at 0x10000.
    //         - If 1, the protected-mode code is loaded at 0x100000.
    //   ...
    buf.seek(SeekFrom::Start(0x0211)).await.ok()?;
    match buf.read_u8().await.ok()? & 0x1 {
        0 => Some(LinuxKernelKind::X86zImage),
        1 => Some(LinuxKernelKind::X86bzImage),
        _ => None,
    }
}

pub(crate) struct LinuxKernel<'a, R: AsyncRead + AsyncSeek + Unpin> {
    buf: &'a mut R,
}

impl<'a, R: AsyncRead + AsyncSeek + Unpin> LinuxKernel<'a, R> {
    pub(crate) fn from_reader(buf: &'a mut R) -> Self {
        LinuxKernel { buf }
    }
}

#[async_trait::async_trait(?Send)]
impl<'a, R: AsyncRead + AsyncSeek + Unpin> VersionFinder for LinuxKernel<'a, R> {
    async fn get_version(&mut self) -> Option<String> {
        match discover_linux_kernel_kind(self.buf).await? {
            LinuxKernelKind::ARMzImage => {
                async fn get_version_from_arm<R: AsyncRead + Unpin>(mut rd: R) -> Option<String> {
                    let mut buffer = Vec::default();
                    compress_tools::tokio_support::uncompress_data(&mut rd, &mut buffer)
                        .await
                        .ok()?;
                    let re = Regex::new(r"Linux version (?P<version>\S+).*").unwrap();
                    re.captures(&buffer)
                        .and_then(|m| m.name("version"))
                        .and_then(|v| str::from_utf8(v.as_bytes()).ok())
                        .map(|v| v.to_string())
                }

                let mut buffer = [0; 0x200];
                loop {
                    let n = self.buf.read(&mut buffer).await.ok()?;

                    // No more data to read
                    if n == 0 {
                        return None;
                    }

                    // Look for compression format header
                    for (offset, window) in buffer[0..n].windows(6).enumerate() {
                        // Headers taken from:
                        // https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux
                        match window {
                            [0x1f, 0x8b, 0x08, ..] => {}               // gzip
                            [0xfd, b'7', b'z', b'X', b'Z', 0x00] => {} // xz
                            [b'B', b'Z', b'h', ..] => {}               // bzip2
                            [0x5d, 0x00, 0x00, ..] => {}               // lzma
                            [0x89, 0x4c, 0x5a, ..] => {}               // lzo
                            [0x02, b'!', b'L', 0x18, ..] => {}         // lz4
                            [b'(', 0xb5, b'/', 0xfd, ..] => {}         // zstd
                            _ => continue,
                        }

                        let mut slice = &buffer[offset..];
                        let current = self.buf.seek(SeekFrom::Current(0)).await.ok()?;
                        let rd = io::AsyncReadExt::chain(&mut slice, &mut self.buf);

                        // Try to get version from uncompressed data
                        if let Some(version) = get_version_from_arm(rd).await {
                            return Some(version);
                        }

                        // Seek back to current position so we can keep looking
                        // for the next compression header
                        self.buf.seek(SeekFrom::Start(current)).await.ok()?;
                    }
                }
            }

            LinuxKernelKind::X86bzImage | LinuxKernelKind::X86zImage => {
                // Taken from: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/x86/boot.txt#n144
                //
                // Offset  Proto   Name            Meaning
                // /Size
                // ...
                // 01F1/1  ALL(1   setup_sects     The size of the setup in sectors
                // ...
                // 020E/2  2.00+   kernel_version  Pointer to kernel version string

                // Get the setup_sects information
                self.buf.seek(SeekFrom::Start(0x01F1)).await.ok()?;
                let setup_sects = u64::from(self.buf.read_u8().await.ok()?);

                // Get kernel_version pointer
                self.buf.seek(SeekFrom::Start(0x020E)).await.ok()?;
                let kernel_version_ptr = u64::from(self.buf.read_u16_le().await.ok()?);

                // Field name:     kernel_version
                // Type:           read
                // Offset/size:    0x20e/2
                // Protocol:       2.00+
                //
                //   If set to a nonzero value, contains a pointer to a NUL-terminated
                //   human-readable kernel version number string, less 0x200.  This can
                //   be used to display the kernel version to the user.  This value
                //   should be less than (0x200*setup_sects).
                if kernel_version_ptr >= setup_sects * 0x200 {
                    return None;
                }

                // Move to the kernel version location
                self.buf
                    .seek(SeekFrom::Start(kernel_version_ptr + 0x200))
                    .await
                    .ok()?;

                // Read the Linux kernel version from the reader
                let mut buffer = [0; 0x200];
                let _ = self.buf.read(&mut buffer).await.ok()?;

                let re = Regex::new(r"(?P<version>\d+.?\.[^\s\u{0}]+)").unwrap();
                re.captures(&buffer)
                    .and_then(|m| m.name("version"))
                    .and_then(|v| str::from_utf8(v.as_bytes()).ok())
                    .map(|v| v.to_string())
            }

            LinuxKernelKind::UImage => {
                // Move to the begin of the file, so we can next read the
                // buffer to match the version.
                self.buf.seek(SeekFrom::Start(0)).await.ok()?;

                // Read the Linux kernel version from the reader
                let mut buffer = [0; 0x200];
                let _ = self.buf.read(&mut buffer).await.ok()?;

                let re = Regex::new(r"(?P<version>\d+.?\.[^\s\u{0}]+)").unwrap();
                re.captures(&buffer)
                    .and_then(|m| m.name("version"))
                    .and_then(|v| str::from_utf8(v.as_bytes()).ok())
                    .map(|v| v.to_string())
            }
        }
    }
}

#[cfg(test)]
mod test {
    use crate::{version, BinaryKind};
    use tokio::io::{AsyncRead, AsyncSeek};

    async fn fixture(name: &str) -> impl AsyncRead + AsyncSeek {
        use tokio::{fs::File, io::BufReader};

        BufReader::new(
            File::open(&format!("tests/fixtures/linuxkernel/{}", name))
                .await
                .unwrap_or_else(|_| panic!("Couldn't open the fixture {}", name)),
        )
    }

    #[tokio::test]
    async fn linux_version() {
        for (f, v) in &[
            ("arm-uImage", "4.1.15-1.2.0+g274a055"),
            ("arm-zImage", "4.4.1"),
            ("x86-bzImage", "4.1.30-1-MANJARO"),
            ("x86-zImage", "4.1.30-1-MANJARO"),
        ] {
            assert_eq!(
                version(&mut fixture(f).await, BinaryKind::LinuxKernel).await,
                Some(v.to_string())
            );
        }
    }
}