diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4af154a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + # Not yet supported. See . + # versioning-strategy: "increase-if-necessary" + ignore: + - dependency-name: "tokio" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "*" + update-types: ["version-update:semver-patch"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b7d5a6..ef59a65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@1.70.0 + - uses: dtolnay/rust-toolchain@1.81 - run: cargo test --all-targets --all-features + - run: cargo test --doc --all-features fmt: name: cargo fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@1.70.0 + - uses: dtolnay/rust-toolchain@1.81 with: components: rustfmt - run: cargo fmt --all --check @@ -30,10 +31,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@1.70.0 + - uses: dtolnay/rust-toolchain@1.81 with: components: clippy - - run: cargo clippy --all-targets -- -D warnings + - run: cargo clippy --all-targets --all-features -- -D warnings allgreen: if: always() diff --git a/Cargo.toml b/Cargo.toml index 8c4c3a4..100dc61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "rockfile", "rockusb" diff --git a/rockfile/examples/rockfile.rs b/rockfile/examples/rockfile.rs index dc0ff5b..80a8225 100644 --- a/rockfile/examples/rockfile.rs +++ b/rockfile/examples/rockfile.rs @@ -22,8 +22,7 @@ fn parse_entry(header: RkBootHeaderEntry, name: &str, file: &mut File) -> Result println!("Name: {}", String::from_utf16(entry.name.as_slice())?); println!("Raw: {:?}", entry); - let mut data = Vec::new(); - data.resize(entry.data_size as usize, 0); + let mut data = vec![0; entry.data_size as usize]; file.seek(SeekFrom::Start(entry.data_offset as u64))?; file.read_exact(&mut data)?; diff --git a/rockusb/Cargo.toml b/rockusb/Cargo.toml index 0a51a0c..629bb66 100644 --- a/rockusb/Cargo.toml +++ b/rockusb/Cargo.toml @@ -11,27 +11,41 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["libusb"] libusb = ["dep:rusb"] +nusb = ["dep:nusb", "dep:futures"] [dependencies] bytes = "1.4.0" crc = "3.0.1" -fastrand = "1.9.0" -num_enum = "0.5.9" +fastrand = "2" +num_enum = "0.7" thiserror = "1.0.38" -rusb = { version = "0.9.1", optional = true } +rusb = { version = "0.9.4", optional = true } +nusb = { version = "0.1.10", optional = true } +futures = { version = "0.3.31", optional = true } [dev-dependencies] anyhow = "1.0.69" -bmap-parser = "0.1.0" +bmap-parser = "0.2.0" clap = { version = "4.2", features = ["derive"] } clap-num = "1.0" flate2 = "1.0.25" -nbd = "0.2.3" +nbd = "0.3" rockfile = { path = "../rockfile", version = "0.1.1" } rusb = "0.9.1" +tokio = { version = "1.40.0", features = ["full"] } +futures = { version = "0.3.31", features = ["compat", "io-compat"]} +tokio-util = { version = "0.7.12", features = ["compat"] } +async-compression = { version = "0.4.5", features = ["gzip", "futures-io"] } + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] [[example]] name="rockusb" required-features = ["libusb"] + +[[example]] +name="rockusb-nusb" +required-features = ["nusb"] diff --git a/rockusb/README.md b/rockusb/README.md index 7b0fd7c..3139bd8 100644 --- a/rockusb/README.md +++ b/rockusb/README.md @@ -2,15 +2,29 @@ Rockchip bootroms and early loaders implement an USB protocol to help loader early firmware, flashing persistant storage etc. This crate contains a sans-io -implementation of that protocol as well as an optional (enabled by default) -implementation of IO using libusb +implementation of that protocol as well as an optional implementations of IO +using libusb or nusb. +Printing chip info using libusb backend: ```rust,no_run -fn main() -> anyhow::Result<()> { - let devices = rockusb::libusb::Devices::new()?; - let mut transport = devices.iter().next() - .ok_or_else(|| anyhow::anyhow!("No Device found"))??; - println!("Chip Info: {:0x?}", transport.chip_info()?); - Ok(()) -} +# fn main() -> anyhow::Result<()> { +let devices = rockusb::libusb::Devices::new()?; +let mut transport = devices.iter().next() + .ok_or_else(|| anyhow::anyhow!("No Device found"))??; +println!("Chip Info: {:0x?}", transport.chip_info()?); +Ok(()) +# } +``` + +Printing chip info using nusb backend: +```rust,no_run +# #[tokio::main] +# async fn main() -> anyhow::Result<()> { +let mut devices = rockusb::nusb::devices()?; +let info = devices.next() + .ok_or_else(|| anyhow::anyhow!("No Device found"))?; +let mut transport = rockusb::nusb::Transport::from_usb_device_info(info)?; +println!("Chip Info: {:0x?}", transport.chip_info().await?); +Ok(()) +# } ``` diff --git a/rockusb/examples/rockusb-nusb.rs b/rockusb/examples/rockusb-nusb.rs new file mode 100644 index 0000000..8d538f4 --- /dev/null +++ b/rockusb/examples/rockusb-nusb.rs @@ -0,0 +1,372 @@ +use std::{ + ffi::OsStr, + io::SeekFrom, + path::{Path, PathBuf}, + thread::sleep, + time::Duration, +}; + +use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use bmap_parser::Bmap; +use clap::{Parser, ValueEnum}; +use clap_num::maybe_hex; +use futures::io::{BufReader, BufWriter}; +use rockfile::boot::{ + RkBootEntry, RkBootEntryBytes, RkBootHeader, RkBootHeaderBytes, RkBootHeaderEntry, +}; +use rockusb::nusb::Transport; +use rockusb::protocol::ResetOpcode; +use tokio::{ + fs::File, + io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}, +}; +use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; + +async fn read_flash_info(mut transport: Transport) -> Result<()> { + let info = transport.flash_info().await?; + println!("Raw Flash Info: {:0x?}", info); + println!( + "Flash size: {} MB ({} sectors)", + info.sectors() / 2048, + info.sectors() + ); + + Ok(()) +} + +async fn reset_device(mut transport: Transport, opcode: ResetOpcode) -> Result<()> { + transport.reset_device(opcode).await?; + Ok(()) +} + +async fn read_chip_info(mut transport: Transport) -> Result<()> { + println!("Chip Info: {:0x?}", transport.chip_info().await?); + Ok(()) +} + +async fn read_lba(mut transport: Transport, offset: u32, length: u16, path: &Path) -> Result<()> { + let mut data = vec![0; length as usize * 512]; + transport.read_lba(offset, &mut data).await?; + + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .await?; + file.write_all(&data).await?; + Ok(()) +} + +async fn write_lba(mut transport: Transport, offset: u32, length: u16, path: &Path) -> Result<()> { + let mut data = vec![0; length as usize * 512]; + + let mut file = File::open(path).await?; + file.read_exact(&mut data).await?; + + transport.write_lba(offset, &data).await?; + + Ok(()) +} + +async fn read_file(transport: Transport, offset: u32, length: u16, path: &Path) -> Result<()> { + let mut file = tokio::fs::File::create(path).await?; + let mut io = transport.into_io().await?.compat(); + + io.seek(SeekFrom::Start(offset as u64 * 512)).await?; + tokio::io::copy(&mut io.take(length as u64 * 512), &mut file).await?; + Ok(()) +} + +async fn write_file(transport: Transport, offset: u32, path: &Path) -> Result<()> { + let mut file = tokio::fs::File::open(path).await?; + let mut io = transport.into_io().await?.compat(); + + io.seek(SeekFrom::Start(offset as u64 * 512)).await?; + tokio::io::copy(&mut file, &mut io).await?; + Ok(()) +} + +fn find_bmap(img: &Path) -> Option { + fn append(path: PathBuf) -> PathBuf { + let mut p = path.into_os_string(); + p.push(".bmap"); + p.into() + } + + let mut bmap = img.to_path_buf(); + loop { + bmap = append(bmap); + if bmap.exists() { + return Some(bmap); + } + // Drop .bmap + bmap.set_extension(""); + bmap.extension()?; + // Drop existing orignal extension part + bmap.set_extension(""); + } +} + +async fn write_bmap(transport: Transport, path: &Path) -> Result<()> { + let bmap_path = find_bmap(path).ok_or_else(|| anyhow!("Failed to find bmap"))?; + println!("Using bmap file: {}", path.display()); + + let mut bmap_file = File::open(bmap_path).await?; + let mut xml = String::new(); + bmap_file.read_to_string(&mut xml).await?; + let bmap = Bmap::from_xml(&xml)?; + + // HACK to minimize small writes + let mut writer = BufWriter::with_capacity(16 * 1024 * 1024, transport.into_io().await?); + + let file = File::open(path).await?; + let mut file = BufReader::with_capacity(16 * 1024 * 1024, file.compat()); + match path.extension().and_then(OsStr::to_str) { + Some("gz") => { + let gz = GzipDecoder::new(file); + let mut gz = bmap_parser::AsyncDiscarder::new(gz); + bmap_parser::copy_async(&mut gz, &mut writer, &bmap).await?; + } + _ => { + bmap_parser::copy_async(&mut file, &mut writer, &bmap).await?; + } + } + + Ok(()) +} + +async fn download_entry( + header: RkBootHeaderEntry, + code: u16, + file: &mut File, + transport: &mut Transport, +) -> Result<()> { + for i in 0..header.count { + let mut entry: RkBootEntryBytes = [0; 57]; + + file.seek(SeekFrom::Start( + header.offset as u64 + (header.size * i) as u64, + )) + .await?; + file.read_exact(&mut entry).await?; + + let entry = RkBootEntry::from_bytes(&entry); + println!("{} Name: {}", i, String::from_utf16(entry.name.as_slice())?); + + let mut data = vec![0; entry.data_size as usize]; + + file.seek(SeekFrom::Start(entry.data_offset as u64)).await?; + file.read_exact(&mut data).await?; + + transport.write_maskrom_area(code, &data).await?; + + println!("Done!... waiting {}ms", entry.data_delay); + if entry.data_delay > 0 { + sleep(Duration::from_millis(entry.data_delay as u64)); + } + } + + Ok(()) +} + +async fn download_boot(mut transport: Transport, path: &Path) -> Result<()> { + let mut file = File::open(path).await?; + let mut header: RkBootHeaderBytes = [0; 102]; + file.read_exact(&mut header).await?; + + let header = + RkBootHeader::from_bytes(&header).ok_or_else(|| anyhow!("Failed to parse header"))?; + + download_entry(header.entry_471, 0x471, &mut file, &mut transport).await?; + download_entry(header.entry_472, 0x472, &mut file, &mut transport).await?; + + Ok(()) +} + +#[derive(Debug, clap::Parser)] +enum Command { + List, + DownloadBoot { + path: PathBuf, + }, + Read { + #[clap(value_parser=maybe_hex::)] + offset: u32, + #[clap(value_parser=maybe_hex::)] + length: u16, + path: PathBuf, + }, + ReadFile { + #[clap(value_parser=maybe_hex::)] + offset: u32, + #[clap(value_parser=maybe_hex::)] + length: u16, + path: PathBuf, + }, + Write { + #[clap(value_parser=maybe_hex::)] + offset: u32, + #[clap(value_parser=maybe_hex::)] + length: u16, + path: PathBuf, + }, + WriteFile { + #[clap(value_parser=maybe_hex::)] + offset: u32, + path: PathBuf, + }, + WriteBmap { + path: PathBuf, + }, + ChipInfo, + FlashId, + FlashInfo, + ResetDevice { + #[clap(value_enum, default_value_t=ArgResetOpcode::Reset)] + opcode: ArgResetOpcode, + }, +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum ArgResetOpcode { + /// Reset + Reset, + /// Reset to USB mass-storage device class + MSC, + /// Powers the SOC off + PowerOff, + /// Reset to maskrom mode + Maskrom, + /// Disconnect from USB + Disconnect, +} + +impl From for ResetOpcode { + fn from(arg: ArgResetOpcode) -> ResetOpcode { + match arg { + ArgResetOpcode::Reset => ResetOpcode::Reset, + ArgResetOpcode::MSC => ResetOpcode::MSC, + ArgResetOpcode::PowerOff => ResetOpcode::PowerOff, + ArgResetOpcode::Maskrom => ResetOpcode::Maskrom, + ArgResetOpcode::Disconnect => ResetOpcode::Disconnect, + } + } +} + +#[derive(Debug, Clone)] +struct DeviceArg { + bus_number: u8, + address: u8, +} + +fn parse_device(device: &str) -> Result { + let mut parts = device.split(':'); + let bus_number = parts + .next() + .ok_or_else(|| anyhow!("No bus number: use :
"))? + .parse() + .map_err(|_| anyhow!("Bus should be a number"))?; + let address = parts + .next() + .ok_or_else(|| anyhow!("No address: use :
"))? + .parse() + .map_err(|_| anyhow!("Address should be a numbrer"))?; + if parts.next().is_some() { + return Err(anyhow!("Too many parts")); + } + Ok(DeviceArg { + bus_number, + address, + }) +} + +#[derive(clap::Parser)] +struct Opts { + #[arg(short, long, value_parser = parse_device)] + /// Device type specified as :
+ device: Option, + #[command(subcommand)] + command: Command, +} + +fn list_available_devices() -> Result<()> { + let devices = rockusb::nusb::devices()?; + println!("Available rockchip devices:"); + for d in devices { + println!( + "* Bus {} Device {} ID {}:{}", + d.bus_number(), + d.device_address(), + d.vendor_id(), + d.product_id() + ); + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + let opt = Opts::parse(); + + // Commands that don't talk a device + if matches!(opt.command, Command::List) { + return list_available_devices(); + } + + let mut devices = rockusb::nusb::devices()?; + let info = if let Some(dev) = opt.device { + devices + .find(|d| d.bus_number() == dev.bus_number && d.device_address() == dev.address) + .ok_or_else(|| anyhow!("Specified device not found"))? + } else { + let mut devices: Vec<_> = devices.collect(); + match devices.len() { + 0 => Err(anyhow!("No devices found")), + 1 => Ok(devices.pop().unwrap()), + _ => { + drop(devices); + let _ = list_available_devices(); + println!(); + Err(anyhow!( + "Please select a specific device using the -d option" + )) + } + }? + }; + + let mut transport = Transport::from_usb_device_info(info)?; + + match opt.command { + Command::List => unreachable!(), + Command::DownloadBoot { path } => download_boot(transport, &path).await, + Command::Read { + offset, + length, + path, + } => read_lba(transport, offset, length, &path).await, + Command::ReadFile { + offset, + length, + path, + } => read_file(transport, offset, length, &path).await, + Command::Write { + offset, + length, + path, + } => write_lba(transport, offset, length, &path).await, + Command::WriteFile { offset, path } => write_file(transport, offset, &path).await, + Command::WriteBmap { path } => write_bmap(transport, &path).await, + Command::ChipInfo => read_chip_info(transport).await, + Command::FlashId => { + let id = transport.flash_id().await?; + println!("Flash id: {}", id.to_str()); + println!("raw: {:?}", id); + Ok(()) + } + Command::FlashInfo => read_flash_info(transport).await, + Command::ResetDevice { opcode } => reset_device(transport, opcode.into()).await, + } +} diff --git a/rockusb/examples/rockusb.rs b/rockusb/examples/rockusb.rs index 7efd238..16c6793 100644 --- a/rockusb/examples/rockusb.rs +++ b/rockusb/examples/rockusb.rs @@ -42,8 +42,7 @@ fn read_chip_info(mut transport: Transport) -> Result<()> { } fn read_lba(mut transport: Transport, offset: u32, length: u16, path: &Path) -> Result<()> { - let mut data = Vec::new(); - data.resize(length as usize * 512, 0); + let mut data = vec![0; length as usize * 512]; transport.read_lba(offset, &mut data)?; let mut file = std::fs::OpenOptions::new() @@ -56,8 +55,7 @@ fn read_lba(mut transport: Transport, offset: u32, length: u16, path: &Path) -> } fn write_lba(mut transport: Transport, offset: u32, length: u16, path: &Path) -> Result<()> { - let mut data = Vec::new(); - data.resize(length as usize * 512, 0); + let mut data = vec![0; length as usize * 512]; let mut file = File::open(path)?; file.read_exact(&mut data)?; @@ -141,8 +139,7 @@ fn download_entry( let entry = RkBootEntry::from_bytes(&entry); println!("{} Name: {}", i, String::from_utf16(entry.name.as_slice())?); - let mut data = Vec::new(); - data.resize(entry.data_size as usize, 0); + let mut data = vec![0; entry.data_size as usize]; file.seek(SeekFrom::Start(entry.data_offset as u64))?; file.read_exact(&mut data)?; @@ -190,15 +187,19 @@ fn run_nbd(transport: Transport) -> Result<()> { let io = transport.into_io()?; - let export = nbd::Export { - size: io.size(), - readonly: false, - ..Default::default() - }; - println!("Connection!"); - nbd::server::handshake(&mut stream, &export)?; + nbd::server::handshake(&mut stream, |_s| { + Ok(nbd::Export { + size: io.size(), + readonly: false, + resizeable: false, + rotational: false, + send_trim: false, + send_flush: true, + data: (), + }) + })?; println!("Shook hands!"); nbd::server::transmission(&mut stream, io)?; println!("nbd client disconnected"); diff --git a/rockusb/src/lib.rs b/rockusb/src/lib.rs index 031ea89..92780f9 100644 --- a/rockusb/src/lib.rs +++ b/rockusb/src/lib.rs @@ -1,9 +1,13 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] /// libusb transport implementation #[cfg(feature = "libusb")] pub mod libusb; -/// sans-io protocol implementationsss +/// nusb transport implementation +#[cfg(feature = "nusb")] +pub mod nusb; +/// sans-io protocol implementations /// /// This module contains all protocol logic; Each operation implements the [operation::OperationSteps] /// trait which gives a transport a series of [operation::UsbStep] to execute to complete an diff --git a/rockusb/src/libusb.rs b/rockusb/src/libusb.rs index 4db85e9..68b8c8b 100644 --- a/rockusb/src/libusb.rs +++ b/rockusb/src/libusb.rs @@ -84,7 +84,7 @@ pub struct Transport { impl Transport { fn new( - mut handle: DeviceHandle, + handle: DeviceHandle, interface: u8, ep_in: u8, ep_out: u8, @@ -148,14 +148,14 @@ impl Transport { }) } - /// Create an IO object which implements [Read](std::io::Read), [Write](std::io::Write) and - /// [Seek](std::io::Seek) + /// Create an IO object which implements [Read], [Write] and + /// [Seek] pub fn io(&mut self) -> Result> { TransportIO::new(self) } - /// Convert into an IO object which implements [Read](std::io::Read), [Write](std::io::Write) and - /// [Seek](std::io::Seek) + /// Convert into an IO object which implements [Read], [Write] and + /// [Seek] pub fn into_io(self) -> Result> { TransportIO::new(self) } @@ -230,8 +230,8 @@ impl Transport { /// read from the flash /// - /// start_sector with [SECTOR_SIZE](crate::protocol::SECTOR_SIZE) sectors. the data to be read - /// must be a multiple of [SECTOR_SIZE](crate::protocol::SECTOR_SIZE) bytes + /// start_sector with [SECTOR_SIZE] sectors. the data to be read + /// must be a multiple of [SECTOR_SIZE] bytes pub fn read_lba(&mut self, start_sector: u32, read: &mut [u8]) -> Result { self.handle_operation(crate::operation::read_lba(start_sector, read)) .map(|t| t.into()) @@ -239,8 +239,8 @@ impl Transport { /// Create operation to read an lba from the flash /// - /// start_sector based on [SECTOR_SIZE](crate::protocol::SECTOR_SIZE) sectors. the data to be - /// written must be a multiple of [SECTOR_SIZE](crate::protocol::SECTOR_SIZE) bytes + /// start_sector based on [SECTOR_SIZE] sectors. the data to be + /// written must be a multiple of [SECTOR_SIZE] bytes pub fn write_lba(&mut self, start_sector: u32, write: &[u8]) -> Result { self.handle_operation(crate::operation::write_lba(start_sector, write)) .map(|t| t.into()) @@ -258,8 +258,7 @@ impl Transport { } } -/// IO object which implements [Read](std::io::Read), [Write](std::io::Write) and -/// [Seek](std::io::Seek) +/// IO object which implements [Read], [Write] and [Seek] pub struct TransportIO { transport: T, size: u64, diff --git a/rockusb/src/nusb.rs b/rockusb/src/nusb.rs new file mode 100644 index 0000000..2ff50ae --- /dev/null +++ b/rockusb/src/nusb.rs @@ -0,0 +1,566 @@ +use std::io::SeekFrom; +use std::{borrow::BorrowMut, task::Poll}; + +use crate::{ + operation::{OperationSteps, UsbStep}, + protocol::{ChipInfo, FlashId, FlashInfo, ResetOpcode, SECTOR_SIZE}, +}; +use futures::{future::BoxFuture, ready}; +use futures::{AsyncRead, AsyncSeek, AsyncWrite}; +use nusb::{ + transfer::{ControlOut, ControlType, Recipient, RequestBuffer}, + DeviceInfo, +}; +use thiserror::Error; + +/// Error indicate a device is not available +#[derive(Debug, Error)] +#[error("Device is not available: {error}")] +pub struct DeviceUnavalable { + #[from] + pub error: nusb::Error, +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Usb error: {0}")] + UsbError(#[from] nusb::Error), + #[error("Usb transfer error: {0}")] + UsbTransferError(#[from] nusb::transfer::TransferError), + #[error("Operation error: {0}")] + OperationError(#[from] crate::operation::UsbOperationError), +} +type Result = std::result::Result; + +/// List rockchip devices +pub fn devices() -> std::result::Result, nusb::Error> { + Ok(nusb::list_devices()?.filter(|d| d.vendor_id() == 0x2207)) +} + +/// nusb based Transport for rockusb operation +pub struct Transport { + interface: nusb::Interface, + ep_in: u8, + ep_out: u8, +} + +impl Transport { + fn new( + device: nusb::Device, + interface: u8, + ep_in: u8, + ep_out: u8, + ) -> std::result::Result { + let interface = device.claim_interface(interface)?; + Ok(Self { + interface, + ep_in, + ep_out, + }) + } + + /// Create a new transport from a device info + pub fn from_usb_device_info( + info: nusb::DeviceInfo, + ) -> std::result::Result { + let device = info.open()?; + Self::from_usb_device(device) + } + + /// Create a new transport from an existing device + pub fn from_usb_device(device: nusb::Device) -> std::result::Result { + for config in device.clone().configurations() { + for interface in config.interface_alt_settings() { + let output = interface.endpoints().find(|e| { + e.direction() == nusb::transfer::Direction::Out + && e.transfer_type() == nusb::transfer::EndpointType::Bulk + }); + let input = interface.endpoints().find(|e| { + e.direction() == nusb::transfer::Direction::In + && e.transfer_type() == nusb::transfer::EndpointType::Bulk + }); + + if let (Some(input), Some(output)) = (input, output) { + return Transport::new( + device, + interface.interface_number(), + input.address(), + output.address(), + ); + } + } + } + Err(DeviceUnavalable { + error: nusb::Error::new(std::io::ErrorKind::NotFound, "Device not found"), + }) + } + + /// Convert into an IO object which implements [AsyncRead], + /// [AsyncWrite] and [AsyncSeek] + pub async fn into_io(self) -> Result { + TransportIO::new(self).await + } + + async fn handle_operation(&mut self, mut operation: O) -> Result + where + O: OperationSteps, + { + loop { + let step = operation.step(); + match step { + UsbStep::WriteBulk { data } => { + let _written = self + .interface + .bulk_out(self.ep_out, data.to_vec()) + .await + .into_result()?; + } + UsbStep::ReadBulk { data } => { + let req = RequestBuffer::new(data.len()); + let read = self + .interface + .bulk_in(self.ep_in, req) + .await + .into_result()?; + data.copy_from_slice(&read); + } + UsbStep::WriteControl { + request_type, + request, + value, + index, + data, + } => { + let (control_type, recipient) = ( + match request_type >> 5 & 0x03 { + 0 => ControlType::Standard, + 1 => ControlType::Class, + 2 => ControlType::Vendor, + _ => ControlType::Standard, + }, + match request_type & 0x1f { + 0 => Recipient::Device, + 1 => Recipient::Interface, + 2 => Recipient::Endpoint, + 3 => Recipient::Other, + _ => Recipient::Device, + }, + ); + let data = ControlOut { + control_type, + recipient, + request, + value, + index, + data, + }; + self.interface.control_out(data).await.into_result()?; + } + UsbStep::Finished(r) => break r.map_err(|e| e.into()), + } + } + } + + /// retrieve SoC flash identifier + pub async fn flash_id(&mut self) -> Result { + self.handle_operation(crate::operation::flash_id()).await + } + + /// retrieve SoC flash info + pub async fn flash_info(&mut self) -> Result { + self.handle_operation(crate::operation::flash_info()).await + } + + /// retrieve SoC chip info + pub async fn chip_info(&mut self) -> Result { + self.handle_operation(crate::operation::chip_info()).await + } + + /// read from the flash + /// + /// start_sector with [SECTOR_SIZE] sectors. the data to be read + /// must be a multiple of [SECTOR_SIZE] bytes + pub async fn read_lba(&mut self, start_sector: u32, read: &mut [u8]) -> Result { + self.handle_operation(crate::operation::read_lba(start_sector, read)) + .await + .map(|t| t.into()) + } + + /// Create operation to read an lba from the flash + /// + /// start_sector based on [SECTOR_SIZE] sectors. the data to be + /// written must be a multiple of [SECTOR_SIZE] bytes + pub async fn write_lba(&mut self, start_sector: u32, write: &[u8]) -> Result { + self.handle_operation(crate::operation::write_lba(start_sector, write)) + .await + .map(|t| t.into()) + } + + /// Write a specific area while in maskrom mode; typically 0x471 or 0x472 data as retrieved from a + /// rockchip boot file + pub async fn write_maskrom_area(&mut self, area: u16, data: &[u8]) -> Result<()> { + self.handle_operation(crate::operation::write_area(area, data)) + .await + } + + /// Reset the device + pub async fn reset_device(&mut self, opcode: ResetOpcode) -> Result<()> { + self.handle_operation(crate::operation::reset_device(opcode)) + .await + } +} + +type ReadResult = std::io::Result<(Vec, usize)>; +enum IoState { + Idle(Option), + Read(BoxFuture<'static, (TransportIOInner, ReadResult)>), + Write(BoxFuture<'static, (TransportIOInner, std::io::Result)>), + Flush(BoxFuture<'static, (TransportIOInner, std::io::Result<()>)>), +} + +struct TransportIOInner { + transport: Transport, + // Read/Write offset in bytes + offset: u64, + buffer: Box<[u8; 512]>, + size: u64, + // Whether or not the buffer is dirty + state: BufferState, +} + +/// IO object which implements [AsyncRead], [AsyncWrite] and [AsyncSeek] +pub struct TransportIO { + // io execution state + io_state: IoState, + size: u64, +} + +impl TransportIO { + /// Create a new IO object around a given transport + pub async fn new(mut transport: Transport) -> Result { + let info = transport.borrow_mut().flash_info().await?; + let size = info.size(); + let inner = TransportIOInner { + transport, + offset: 0, + buffer: Box::new([0u8; 512]), + size, + state: BufferState::Invalid, + }; + Ok(Self { + size, + io_state: IoState::Idle(Some(inner)), + }) + } + + // Convert into the inner transport + // + // Panics if the the TransportIO is currently executing I/O operations + pub fn into_inner(self) -> Transport { + let inner = match self.io_state { + IoState::Idle(Some(i)) => i, + _ => panic!("TransportIO is currently executing I/O operations"), + }; + inner.transport + } + + // Size of the flash in bytes + pub fn size(&self) -> u64 { + self.size + } +} + +impl TransportIOInner { + const MAXIO_SIZE: u64 = 128 * crate::protocol::SECTOR_SIZE; + fn current_sector(&self) -> u64 { + self.offset / SECTOR_SIZE + } + + // Want to start an i/o operation with a given maximum length + async fn pre_io(&mut self, len: u64) -> std::result::Result { + if self.offset >= self.size { + return Ok(IOOperation::Eof); + } + + // Offset inside the current sector + let sector_offset = self.offset % SECTOR_SIZE; + // bytes left from current position to end of current sector + let sector_remaining = SECTOR_SIZE - sector_offset; + + // If the I/O operation is starting at a sector edge and encompasses at least one sector + // then direct I/O can be done + if sector_offset == 0 && len >= SECTOR_SIZE { + // At most read the amount of bytes left + let left = self.size - self.offset; + let io_len = len.min(left) / SECTOR_SIZE * SECTOR_SIZE; + Ok(IOOperation::Direct { + len: io_len.min(Self::MAXIO_SIZE) as usize, + }) + } else { + if self.state == BufferState::Invalid { + let sector = self.current_sector() as u32; + self.transport + .borrow_mut() + .read_lba(sector, self.buffer.as_mut()) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))?; + self.state = BufferState::Valid; + } + Ok(IOOperation::Buffered { + offset: sector_offset as usize, + len: len.min(sector_remaining) as usize, + }) + } + } + + async fn post_io(&mut self, len: u64) -> std::result::Result { + // Offset inside the current sector + let sector_offset = self.offset % SECTOR_SIZE; + // bytes left from current position to end of current sector + let sector_remaining = SECTOR_SIZE - sector_offset; + + // If going over the sector edge flush the current buffer and invalidate it + if len >= sector_remaining { + self.flush_buffer().await?; + self.state = BufferState::Invalid; + } + self.offset += len; + Ok(len as usize) + } + + async fn flush_buffer(&mut self) -> std::io::Result<()> { + if self.state == BufferState::Dirty { + let sector = self.current_sector() as u32; + self.transport + .borrow_mut() + .write_lba(sector, self.buffer.as_mut()) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))?; + self.state = BufferState::Valid; + } + Ok(()) + } + + async fn do_read(&mut self, buf: &mut [u8]) -> std::io::Result { + let sector = self.current_sector() as u32; + self.transport + .borrow_mut() + .read_lba(sector, buf) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))?; + Ok(buf.len()) + } + + async fn do_write(&mut self, buf: &[u8]) -> std::io::Result { + let sector = self.current_sector() as u32; + self.transport + .borrow_mut() + .write_lba(sector, buf) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))?; + Ok(buf.len()) + } +} + +enum IOOperation { + Direct { len: usize }, + Buffered { offset: usize, len: usize }, + Eof, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum BufferState { + // Buffer content doesn't match current offset + Invalid, + // Buffer content matches offset and device-side + Valid, + // Buffer content matches offset and has outstanding data + Dirty, +} + +impl AsyncWrite for TransportIO { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let me = self.get_mut(); + loop { + match me.io_state { + IoState::Idle(ref mut inner) => { + let mut inner = inner.take().unwrap(); + let buf = + Vec::from(&buf[0..buf.len().min(TransportIOInner::MAXIO_SIZE as usize)]); + me.io_state = IoState::Write(Box::pin(async move { + let io = match inner.pre_io(buf.len() as u64).await { + Ok(io) => io, + Err(e) => return (inner, Err(e)), + }; + let r = match io { + IOOperation::Direct { len } => { + match inner.do_write(&buf[..len]).await { + Ok(r) => r, + Err(e) => return (inner, Err(e)), + } + } + IOOperation::Buffered { offset, len } => { + inner.buffer[offset..offset + len].copy_from_slice(&buf[0..len]); + inner.state = BufferState::Dirty; + len + } + IOOperation::Eof => { + return ( + inner, + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Trying to write past end of area", + )), + ) + } + }; + let r = inner.post_io(r as u64).await; + (inner, r) + })) + } + IoState::Write(ref mut f) => { + let (inner, r) = ready!(f.as_mut().poll(cx)); + me.io_state = IoState::Idle(Some(inner)); + return Poll::Ready(r); + } + _ => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Invalid transport state", + ))) + } + } + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let me = self.get_mut(); + loop { + match me.io_state { + IoState::Idle(ref mut inner) => { + let mut inner = inner.take().unwrap(); + me.io_state = IoState::Flush(Box::pin(async move { + let r = inner.flush_buffer().await; + (inner, r) + })) + } + IoState::Flush(ref mut f) => { + let (inner, r) = ready!(f.as_mut().poll(cx)); + me.io_state = IoState::Idle(Some(inner)); + return Poll::Ready(r); + } + _ => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Invalid transport state", + ))) + } + } + } + } + + fn poll_close( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.poll_flush(cx) + } +} + +impl AsyncRead for TransportIO { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + let me = self.get_mut(); + if let IoState::Idle(ref mut inner) = me.io_state { + let mut inner = inner.take().unwrap(); + let mut buf = vec![0; buf.len()]; + me.io_state = IoState::Read(Box::pin(async move { + let io = match inner.pre_io(buf.len() as u64).await { + Ok(io) => io, + Err(e) => return (inner, Err(e)), + }; + let r = match io { + IOOperation::Direct { len } => match inner.do_read(&mut buf[..len]).await { + Ok(r) => r, + Err(e) => return (inner, Err(e)), + }, + IOOperation::Buffered { offset, len } => { + buf[0..len].copy_from_slice(&inner.buffer[offset..offset + len]); + len + } + IOOperation::Eof => 0, + }; + let r = inner.post_io(r as u64).await.map(|r| (buf, r)); + (inner, r) + })) + } + + match me.io_state { + IoState::Read(ref mut f) => { + let (inner, r) = ready!(f.as_mut().poll(cx)); + me.io_state = IoState::Idle(Some(inner)); + let r = match r { + Ok((read_buf, r)) => { + buf[..r].copy_from_slice(&read_buf[..r]); + Ok(r) + } + Err(e) => Err(e), + }; + Poll::Ready(r) + } + _ => Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Invalid transport state", + ))), + } + } +} + +impl AsyncSeek for TransportIO { + fn poll_seek( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + pos: SeekFrom, + ) -> std::task::Poll> { + let me = self.get_mut(); + match me.io_state { + IoState::Idle(Some(ref mut inner)) => { + inner.offset = match pos { + SeekFrom::Start(offset) => inner.size.min(offset), + SeekFrom::End(offset) => { + if offset > 0 { + inner.size + } else { + let offset = offset.unsigned_abs(); + inner.size.saturating_sub(offset) + } + } + SeekFrom::Current(offset) => { + if offset > 0 { + let offset = offset as u64; + inner.offset.saturating_add(offset).min(inner.size) + } else { + let offset = offset.unsigned_abs(); + inner.offset.saturating_sub(offset) + } + } + }; + Poll::Ready(Ok(inner.offset)) + } + _ => Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Invalid transport state", + ))), + } + } +} diff --git a/rust-analyzer.json b/rust-analyzer.json new file mode 100644 index 0000000..0d2875b --- /dev/null +++ b/rust-analyzer.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.features": "all" +}