| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! This is a migration tool to help move from C-macro style bind rules to using the bind compiler. |
| |
| use regex::Regex; |
| use std::collections::HashSet; |
| use std::convert::TryFrom; |
| use std::fs::File; |
| use std::fs::OpenOptions; |
| use std::io::{Read, Seek, SeekFrom, Write}; |
| use std::path::PathBuf; |
| use structopt::StructOpt; |
| |
| #[derive(StructOpt, Debug)] |
| struct Opt { |
| #[structopt(parse(from_os_str))] |
| input: PathBuf, |
| } |
| |
| #[derive(Debug)] |
| struct DriverArgs { |
| driver_name: String, |
| driver_ops: String, |
| vendor: String, |
| version: String, |
| num_ops: usize, |
| |
| match_start: usize, |
| match_end: usize, |
| } |
| |
| impl TryFrom<&str> for DriverArgs { |
| type Error = &'static str; |
| |
| fn try_from(input: &str) -> Result<Self, Self::Error> { |
| let begin_re = |
| Regex::new(r"ZIRCON_DRIVER_BEGIN\(([^,]*),([^,]*),([^,]*),([^,]*),([^\)]*)\)").unwrap(); |
| |
| let header = begin_re.captures(input).ok_or("Couldn't find driver begin macro")?; |
| |
| let get_group_as_string = |i: usize| -> Result<String, Self::Error> { |
| Ok(header.get(i).ok_or("Driver begin macro regex failed")?.as_str().trim().to_string()) |
| }; |
| |
| let driver_name = get_group_as_string(1)?; |
| let driver_ops = get_group_as_string(2)?; |
| let vendor = get_group_as_string(3)?; |
| let version = get_group_as_string(4)?; |
| let num_ops = get_group_as_string(5)? |
| .parse::<usize>() |
| .map_err(|_| "Couldn't parse number of bind ops")?; |
| |
| let mat = header.get(0).ok_or("Driver begin macro regex failed")?; |
| Ok(DriverArgs { |
| driver_name, |
| driver_ops, |
| vendor, |
| version, |
| num_ops, |
| match_start: mat.start(), |
| match_end: mat.end(), |
| }) |
| } |
| } |
| |
| #[derive(Debug, PartialEq)] |
| enum Condition { |
| Always, |
| Equals, |
| NotEquals, |
| GreaterThan, |
| LessThan, |
| GreaterThanOrEqual, |
| LessThanOrEqual, |
| } |
| |
| impl TryFrom<&str> for Condition { |
| type Error = &'static str; |
| |
| fn try_from(input: &str) -> Result<Self, Self::Error> { |
| match input { |
| "AL" => Ok(Condition::Always), |
| "EQ" => Ok(Condition::Equals), |
| "NE" => Ok(Condition::NotEquals), |
| "GT" => Ok(Condition::GreaterThan), |
| "LT" => Ok(Condition::LessThan), |
| "GE" => Ok(Condition::GreaterThanOrEqual), |
| "LE" => Ok(Condition::LessThanOrEqual), |
| _ => Err("Unrecognised condition"), |
| } |
| } |
| } |
| |
| #[derive(Debug, PartialEq, Eq, Hash)] |
| enum Library { |
| Acpi, |
| Bluetooth, |
| Composite, |
| Pci, |
| Platform, |
| Serial, |
| Test, |
| Usb, |
| Wlan, |
| } |
| |
| impl Library { |
| fn name(&self) -> &str { |
| match self { |
| Library::Acpi => "fuchsia.acpi", |
| Library::Bluetooth => "fuchsia.bluetooth", |
| Library::Composite => "fuchsia.composite", |
| Library::Pci => "fuchsia.pci", |
| Library::Platform => "fuchsia.platform", |
| Library::Serial => "fuchsia.serial", |
| Library::Test => "fuchsia.test", |
| Library::Usb => "fuchsia.usb", |
| Library::Wlan => "fuchsia.wlan", |
| } |
| } |
| |
| fn build_target(&self) -> &str { |
| match self { |
| Library::Acpi => "//src/devices/bind/fuchsia.acpi", |
| Library::Bluetooth => "//src/devices/bind/fuchsia.bluetooth", |
| Library::Composite => "//src/devices/bind/fuchsia.composite", |
| Library::Pci => "//src/devices/bind/fuchsia.pci", |
| Library::Platform => "//src/devices/bind/fuchsia.platform", |
| Library::Serial => "//src/devices/bind/fuchsia.serial", |
| Library::Test => "//src/devices/bind/fuchsia.test", |
| Library::Usb => "//src/devices/bind/fuchsia.usb", |
| Library::Wlan => "//src/devices/bind/fuchsia.wlan", |
| } |
| } |
| } |
| |
| struct Migrator { |
| libraries: HashSet<Library>, |
| } |
| |
| impl Migrator { |
| fn process_build_file(&self, input: PathBuf) -> Result<Vec<PathBuf>, &'static str> { |
| let mut output_path = input.clone(); |
| let mut file = File::open(input).map_err(|_| "Failed to open build file")?; |
| let mut contents = String::new(); |
| file.read_to_string(&mut contents).map_err(|_| "Failed to read build file")?; |
| |
| let driver_module_re = Regex::new("driver_module").unwrap(); |
| let sources_re = Regex::new(r"sources = \[([^\]]*)\]").unwrap(); |
| |
| let module = |
| driver_module_re.find(&contents).ok_or("Couldn't find driver_module in build file")?; |
| let sources = sources_re |
| .captures(&contents[module.start()..]) |
| .ok_or("Couldn't find sources in driver_module target")?; |
| |
| let source_files = sources.get(1).ok_or("Couldn't find sources in driver_module target")?; |
| let mut result = vec![]; |
| for source in source_files.as_str().split(",") { |
| let trimmed = source.trim(); |
| let unquoted = trimmed.trim_matches('"'); |
| if !unquoted.is_empty() { |
| output_path.set_file_name(unquoted); |
| result.push(output_path.clone()); |
| } |
| } |
| Ok(result) |
| } |
| |
| fn insert_build_rule(&self, driver_name: &str, input: PathBuf) -> Result<(), &'static str> { |
| let mut file = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .open(input) |
| .map_err(|_| "Failed to open build file")?; |
| let mut contents = String::new(); |
| file.read_to_string(&mut contents).map_err(|_| "Failed to read build file")?; |
| |
| let import_re = Regex::new(r"import\([^\)]*\)\n").unwrap(); |
| let mut iter = import_re.find_iter(&contents); |
| let first_import = iter.next().ok_or("Couldn't find import list in build file")?; |
| let last_import = iter.last().unwrap_or(first_import); |
| |
| let driver_module_re = Regex::new("driver_module").unwrap(); |
| let module = |
| driver_module_re.find(&contents).ok_or("Couldn't find driver_module in build file")?; |
| |
| let deps_re = Regex::new(r"deps = \[").unwrap(); |
| let deps = |
| deps_re.find(&contents[module.start()..]).ok_or("Couldn't find driver_module deps")?; |
| let deps_start = module.start() + deps.start(); |
| let deps_end = module.start() + deps.end(); |
| |
| let mut output = String::new(); |
| output.push_str(&contents[..first_import.start()]); |
| output.push_str("import(\"//build/bind/bind.gni\")\n"); |
| output.push_str(&contents[first_import.start()..last_import.end()]); |
| output.push_str("\n"); |
| output.push_str(format!("bind_rules(\"{}_bind\") {{\n", driver_name).as_str()); |
| output.push_str(format!(" rules = \"{}.bind\"\n", driver_name).as_str()); |
| output.push_str(format!(" output = \"{}_bind.h\"\n", driver_name).as_str()); |
| output.push_str(format!(" tests = \"tests.json\"\n").as_str()); |
| if !self.libraries.is_empty() { |
| output.push_str(" deps = [\n"); |
| for library in &self.libraries { |
| output.push_str(format!(" \"{}\",\n", library.build_target()).as_str()); |
| } |
| output.push_str(" ]\n"); |
| } |
| output.push_str("}\n"); |
| output.push_str(&contents[last_import.end()..deps_start]); |
| output.push_str(format!("deps = [\n \":{}_bind\",", driver_name).as_str()); |
| output.push_str(&contents[deps_end..]); |
| |
| file.seek(SeekFrom::Start(0)).map_err(|_| "Failed to seek to beginning of build file")?; |
| file.set_len(0).map_err(|_| "Failed to truncate build file")?; |
| file.write_all(output.as_bytes()).map_err(|_| "Failed to write back to build file")?; |
| |
| Ok(()) |
| } |
| |
| // Best effort approach to rename key/values into their bindc equivalents. If the replacement |
| // requires a library, keep track of that. |
| fn rename<'a>(&mut self, original: &'a str) -> &'a str { |
| match original { |
| "BIND_PROTOCOL" => "fuchsia.BIND_PROTOCOL", |
| |
| "ZX_PROTOCOL_ACPI" => { |
| self.libraries.insert(Library::Acpi); |
| "fuchsia.acpi.BIND_PROTOCOL.DEVICE" |
| } |
| "ZX_PROTOCOL_COMPOSITE" => { |
| self.libraries.insert(Library::Composite); |
| "fuchsia.composite.BIND_PROTOCOL.DEVICE" |
| } |
| "ZX_PROTOCOL_PCI" => { |
| self.libraries.insert(Library::Pci); |
| "fuchsia.pci.BIND_PROTOCOL.DEVICE" |
| } |
| "ZX_PROTOCOL_USB_INTERFACE" => { |
| self.libraries.insert(Library::Usb); |
| "fuchsia.usb.BIND_PROTOCOL.INTERFACE" |
| } |
| "ZX_PROTOCOL_USB_FUNCTION" => { |
| self.libraries.insert(Library::Usb); |
| "fuchsia.usb.BIND_PROTOCOL.FUNCTION" |
| } |
| "ZX_PROTOCOL_SERIAL" => { |
| self.libraries.insert(Library::Serial); |
| "fuchsia.serial.BIND_PROTOCOL.DEVICE" |
| } |
| "ZX_PROTOCOL_SERIAL_IMPL" => { |
| self.libraries.insert(Library::Serial); |
| "fuchsia.serial.BIND_PROTOCOL.IMPL" |
| } |
| "ZX_PROTOCOL_SERIAL_IMPL_ASYNC" => { |
| self.libraries.insert(Library::Serial); |
| "fuchsia.serial.BIND_PROTOCOL.IMPL_ASYNC" |
| } |
| "ZX_PROTOCOL_TEST" => { |
| self.libraries.insert(Library::Test); |
| "fuchsia.test.BIND_PROTOCOL.DEVICE" |
| } |
| "ZX_PROTOCOL_TEST_COMPAT_CHILD" => { |
| self.libraries.insert(Library::Test); |
| "fuchsia.test.BIND_PROTOCOL.COMPAT_CHILD" |
| } |
| "ZX_PROTOCOL_TEST_POWER_CHILD" => { |
| self.libraries.insert(Library::Test); |
| "fuchsia.test.BIND_PROTOCOL.POWER_CHILD" |
| } |
| "ZX_PROTOCOL_TEST_PARENT" => { |
| self.libraries.insert(Library::Test); |
| "fuchsia.test.BIND_PROTOCOL.PARENT" |
| } |
| "ZX_PROTOCOL_WLANPHY" => { |
| self.libraries.insert(Library::Wlan); |
| "fuchsia.wlan.BIND_PROTOCOL.PHY" |
| } |
| "ZX_PROTOCOL_WLANPHY_IMPL" => { |
| self.libraries.insert(Library::Wlan); |
| "fuchsia.wlan.BIND_PROTOCOL.PHY_IMPL" |
| } |
| "ZX_PROTOCOL_WLANIF" => { |
| self.libraries.insert(Library::Wlan); |
| "fuchsia.wlan.BIND_PROTOCOL.IF" |
| } |
| "ZX_PROTOCOL_WLANIF_IMPL" => { |
| self.libraries.insert(Library::Wlan); |
| "fuchsia.wlan.BIND_PROTOCOL.IF_IMPL" |
| } |
| "ZX_PROTOCOL_WLANMAC" => { |
| self.libraries.insert(Library::Wlan); |
| "fuchsia.wlan.BIND_PROTOCOL.MAC" |
| } |
| "ZX_PROTOCOL_BT_HCI" => { |
| self.libraries.insert(Library::Bluetooth); |
| "fuchsia.bluetooth.BIND_PROTOCOL.HCI" |
| } |
| "ZX_PROTOCOL_BT_EMULATOR" => { |
| self.libraries.insert(Library::Bluetooth); |
| "fuchsia.bluetooth.BIND_PROTOCOL.EMULATOR" |
| } |
| "ZX_PROTOCOL_BT_TRANSPORT" => { |
| self.libraries.insert(Library::Bluetooth); |
| "fuchsia.bluetooth.BIND_PROTOCOL.TRANSPORT" |
| } |
| "ZX_PROTOCOL_BT_HOST" => { |
| self.libraries.insert(Library::Bluetooth); |
| "fuchsia.bluetooth.BIND_PROTOCOL.HOST" |
| } |
| "ZX_PROTOCOL_BT_GATT_SVC" => { |
| self.libraries.insert(Library::Bluetooth); |
| "fuchsia.bluetooth.BIND_PROTOCOL.GATT_SVC" |
| } |
| |
| "BIND_PCI_VID" => { |
| self.libraries.insert(Library::Pci); |
| "fuchsia.BIND_PCI_VID" |
| } |
| "BIND_PCI_DID" => { |
| self.libraries.insert(Library::Pci); |
| "fuchsia.BIND_PCI_DID" |
| } |
| |
| "BIND_USB_DID" => { |
| self.libraries.insert(Library::Usb); |
| "fuchsia.BIND_USB_DID" |
| } |
| "BIND_USB_PID" => { |
| self.libraries.insert(Library::Usb); |
| "fuchsia.BIND_USB_PID" |
| } |
| "BIND_USB_CLASS" => { |
| self.libraries.insert(Library::Usb); |
| "fuchsia.BIND_USB_CLASS" |
| } |
| "BIND_USB_SUBCLASS" => { |
| self.libraries.insert(Library::Usb); |
| "fuchsia.BIND_USB_SUBCLASS" |
| } |
| |
| "BIND_PLATFORM_DEV_VID" => { |
| self.libraries.insert(Library::Platform); |
| "fuchsia.BIND_PLATFORM_DEV_VID" |
| } |
| "BIND_PLATFORM_DEV_PID" => "fuchsia.BIND_PLATFORM_DEV_PID", |
| "BIND_PLATFORM_DEV_DID" => "fuchsia.BIND_PLATFORM_DEV_DID", |
| |
| "ATHEROS_VID" => "fuchsia.pci.BIND_PCI_VID.ATHEROS", |
| "INTEL_VID" => "fuchsia.pci.BIND_PCI_VID.INTEL", |
| |
| _ => original, |
| } |
| } |
| |
| fn process_source_file(&mut self, input: PathBuf) -> Result<Option<String>, &'static str> { |
| let mut file = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .open(input.clone()) |
| .map_err(|_| "Failed to open build file")?; |
| let mut contents = String::new(); |
| file.read_to_string(&mut contents).map_err(|_| "Failed to read build file")?; |
| |
| // These regular expressions will fail if there are additional parentheses or commas in the |
| // macros, but we're okay with that since this whole tool is best effort. |
| let include_re = Regex::new(r"(?m)^#include <ddk/binding.h>$").unwrap(); |
| let end_re = Regex::new(r"ZIRCON_DRIVER_END\([^\)]*\)").unwrap(); |
| let op_re = Regex::new(r"BI_[A-Z_]*\([^\)]*\)").unwrap(); |
| let abort_re = Regex::new(r"BI_ABORT\(\)").unwrap(); |
| let abort_if_re = Regex::new(r"BI_ABORT_IF\(([A-Z][A-Z]),([^,]*),([^\)]*)\)").unwrap(); |
| let match_if_re = Regex::new(r"BI_MATCH_IF\(([A-Z][A-Z]),([^,]*),([^\)]*)\)").unwrap(); |
| |
| let args = DriverArgs::try_from(contents.as_str()); |
| if !args.is_ok() { |
| return Ok(None); |
| } |
| let args = args?; |
| |
| let mut bind_rules = String::new(); |
| let mut count = 0; |
| let mut iter = op_re.find_iter(&contents); |
| while let Some(mat) = iter.next() { |
| if abort_re.is_match(mat.as_str()) { |
| bind_rules.push_str("abort;") |
| } else if let Some(caps) = abort_if_re.captures(mat.as_str()) { |
| let condition = Condition::try_from(caps.get(1).unwrap().as_str()); |
| let lhs = self.rename(caps.get(2).unwrap().as_str().trim()); |
| let rhs = self.rename(caps.get(3).unwrap().as_str().trim()); |
| let rule = match condition { |
| Ok(Condition::Equals) => Ok(format!("{} != {};\n", lhs, rhs)), |
| Ok(Condition::NotEquals) => Ok(format!("{} == {};\n", lhs, rhs)), |
| _ => Err("Unsupported condition"), |
| }?; |
| bind_rules.push_str(rule.as_str()); |
| } else if let Some(caps) = match_if_re.captures(mat.as_str()) { |
| let condition = Condition::try_from(caps.get(1).unwrap().as_str()); |
| let first_lhs = caps.get(2).unwrap().as_str().trim(); |
| let rhs = self.rename(caps.get(3).unwrap().as_str().trim()); |
| |
| bind_rules.push_str(format!("accept {} {{\n", self.rename(first_lhs)).as_str()); |
| bind_rules.push_str(format!(" {},\n", rhs).as_str()); |
| |
| // We only support BI_MATCH_IF when we can convert it to an accept. So every remaining |
| // op must be BI_MATCH_IF with the same LHS. We also only support EQ as the condition. |
| assert_eq!(condition, Ok(Condition::Equals)); |
| while let Some(mat) = iter.next() { |
| if let Some(caps) = match_if_re.captures(mat.as_str()) { |
| let condition = Condition::try_from(caps.get(1).unwrap().as_str()); |
| assert_eq!(condition, Ok(Condition::Equals)); |
| |
| let lhs = caps.get(2).unwrap().as_str().trim(); |
| let rhs = self.rename(caps.get(3).unwrap().as_str().trim()); |
| assert_eq!(lhs, first_lhs); |
| bind_rules.push_str(format!(" {},\n", rhs).as_str()); |
| } else { |
| return Err("Unsupported bind rules"); |
| } |
| count += 1; |
| } |
| bind_rules.push_str("}\n"); |
| } else { |
| println!("The migration tool doesn't handle this bind op: {}", mat.as_str()); |
| return Err("Unhandled bind op"); |
| } |
| |
| count += 1; |
| } |
| |
| assert_eq!(count, args.num_ops); |
| |
| let mut source_output = String::new(); |
| |
| let mut bind_output_file_path = input.clone(); |
| bind_output_file_path.set_file_name(format!("{}_bind.h", args.driver_name).as_str()); |
| |
| let include = include_re.find(&contents).ok_or("Couldn't find binding include")?; |
| source_output.push_str(&contents[..include.start()]); |
| if let Some(full_path) = bind_output_file_path.to_str() { |
| source_output.push_str(format!("#include \"{}\"\n", full_path).as_str()); |
| } else { |
| source_output.push_str(format!("#include \"{}_bind.h\"\n", args.driver_name).as_str()); |
| } |
| |
| source_output.push_str(&contents[include.end()..args.match_start]); |
| source_output.push_str( |
| format!( |
| "ZIRCON_DRIVER({}, {}, {}, {});\n", |
| args.driver_name, args.driver_ops, args.vendor, args.version |
| ) |
| .as_str(), |
| ); |
| |
| let end = end_re.find(&contents).ok_or("Couldn't find end of driver macro")?; |
| source_output.push_str(&contents[end.end()..]); |
| |
| file.seek(SeekFrom::Start(0)).map_err(|_| "Failed to seek to beginning of source file")?; |
| file.set_len(0).map_err(|_| "Failed to truncate source file")?; |
| file.write_all(source_output.as_bytes()).unwrap(); |
| |
| let mut bind_file_data = String::new(); |
| bind_file_data.push_str("// Copyright 2020 The Fuchsia Authors. All rights reserved.\n"); |
| bind_file_data.push_str( |
| "// Use of this source code is governed by a BSD-style license that can be\n", |
| ); |
| bind_file_data.push_str("// found in the LICENSE file.\n\n"); |
| |
| for library in &self.libraries { |
| bind_file_data.push_str(format!("using {};\n", library.name()).as_str()); |
| } |
| |
| if !self.libraries.is_empty() { |
| bind_file_data.push_str("\n"); |
| } |
| bind_file_data.push_str(bind_rules.as_str()); |
| |
| let mut bind_file_path = input; |
| bind_file_path.set_file_name(format!("{}.bind", args.driver_name).as_str()); |
| let mut bind_file = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .create(true) |
| .open(bind_file_path) |
| .map_err(|_| "Failed to open bind file")?; |
| bind_file |
| .write_all(bind_file_data.as_bytes()) |
| .map_err(|_| "Failed to write to bind file")?; |
| |
| Ok(Some(args.driver_name)) |
| } |
| } |
| |
| fn main() -> Result<(), &'static str> { |
| let opt = Opt::from_iter(std::env::args()); |
| |
| let mut migrator = Migrator { libraries: HashSet::new() }; |
| for source_file in migrator.process_build_file(opt.input.clone())? { |
| if let Some(source_file_str) = source_file.to_str() { |
| println!("Checking {}", source_file_str); |
| } |
| let processed = migrator.process_source_file(source_file)?; |
| if let Some(driver_name) = processed { |
| migrator.insert_build_rule(&driver_name, opt.input)?; |
| return Ok(()); |
| } |
| } |
| |
| Err("Couldn't find a ZIRCON_DRIVER_BEGIN macro") |
| } |