xref: /aosp_15_r20/system/usb_info_tools/typec_connector_class_helper/src/typec_class_utils.rs (revision c6808bf1d92d62e809b5d71fe00befd40d32d6f9)
1 // Copyright 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! typec_class_utils
16 
17 use crate::usb_pd_utils::*;
18 use anyhow::{bail, Context, Result};
19 use log::warn;
20 use regex::Regex;
21 use std::{
22     fs,
23     io::Write,
24     path::{Path, PathBuf},
25 };
26 
27 pub const TYPEC_SYSFS_PATH: &str = "/sys/class/typec";
28 
29 pub const MODE_REGEX: &str = "^mode[0-9]+$";
30 pub const PLUG_ALT_MODE_REGEX: &str = "^port[0-9]+\\-plug[0-9]+\\.[0-9]+$";
31 pub const PLUG_REGEX: &str = "^port[0-9]+\\-plug[0-9]+$";
32 pub const PORT_REGEX: &str = "^port[0-9]+$";
33 pub const PARTNER_ALT_MODE_REGEX: &str = "^port[0-9]+-partner\\.[0-9]+$";
34 pub const PARTNER_PDO_REGEX: &str = "^pd[0-9]+$";
35 pub const PDO_CAPABILITIES_REGEX: &str = "^(sink|source)-capabilities$";
36 pub const PDO_TYPE_REGEX: &str =
37     "^[0-9]+:(battery|fixed_supply|programmable_supply|variable_supply)$";
38 pub const USB_PORT_REGEX: &str = "^usb[0-9]+\\-port[0-9]+$";
39 pub const USB_DEVICE_REGEX: &str = "^[0-9]+\\-[0-9]+(\\.[0-9]+)*$";
40 
41 pub const INDENT_STEP: usize = 2;
42 
43 const ERR_READ_DIR: &str = "Failed to iterate over files in directory";
44 const ERR_READ_FILE: &str = "Cannot read file from path";
45 const ERR_FILE_NAME_FROM_PATH: &str = "Cannot get file name at path";
46 const ERR_FILE_NAME_UTF_INVALID: &str = "The file name is UTF-8 invalid at path";
47 const ERR_DIR_ENTRY_ACCESS: &str = "Cannot access entry in directory";
48 const ERR_WRITE_TO_BUFFER: &str = "Cannot write to output buffer. Attempted to print";
49 const ERR_PATH_NOT_DIR_OR_SYMLINK: &str = "The path is not a directory or a symbolic link";
50 
51 pub struct OutputWriter<W: Write> {
52     buffer: W,
53     indent: usize,
54 }
55 
56 impl<W: Write> OutputWriter<W> {
new(buffer: W, indent: usize) -> Self57     pub fn new(buffer: W, indent: usize) -> Self {
58         Self { buffer, indent }
59     }
60 
61     /// Prints a string with indentation.
print_str_indent(&mut self, str_to_print: &str) -> Result<()>62     fn print_str_indent(&mut self, str_to_print: &str) -> Result<()> {
63         writeln!(self.buffer, "{:indent$}{str_to_print}", "", indent = self.indent)
64             .with_context(|| format!("{ERR_WRITE_TO_BUFFER} {:?}", str_to_print))?;
65         Ok(())
66     }
67 
68     /// Prints a file's contents in a "name: content" format and also adds indentation to multiline strings.
print_file_formatted(&mut self, file_path: &Path) -> Result<()>69     fn print_file_formatted(&mut self, file_path: &Path) -> Result<()> {
70         let file_name = get_path_basename(file_path)?;
71         let file_contents = fs::read_to_string(file_path)
72             .with_context(|| format!("{ERR_READ_FILE} {:?}", file_path))?;
73         let formatted_contents = file_contents
74             .trim_ascii_end()
75             .replace("\n", format!("\n{:indent$}", "", indent = self.indent).as_str());
76         writeln!(
77             self.buffer,
78             "{:indent$}{file_name}: {formatted_contents}",
79             "",
80             indent = self.indent
81         )
82         .with_context(|| format!("{ERR_WRITE_TO_BUFFER} {:?}", formatted_contents))?;
83         Ok(())
84     }
85 
86     /// Prints the name of the directory followed by all the files in it in a "name: content" format.
87     /// The files are sorted alphabetically by path to ensure consistency.
print_dir_files(&mut self, dir_path: &Path) -> Result<()>88     pub fn print_dir_files(&mut self, dir_path: &Path) -> Result<()> {
89         let dir_name = get_path_basename(dir_path)?;
90         self.print_str_indent(dir_name)?;
91 
92         for path in get_sorted_paths_from_dir(dir_path, |path| path.is_file())? {
93             self.indent += INDENT_STEP;
94             let _ = self.print_file_formatted(&path).inspect_err(|err| warn!("{:?}: {err}", path));
95             self.indent -= INDENT_STEP;
96         }
97 
98         Ok(())
99     }
100 }
101 
102 /// Retrieves a sorted vector of paths in the given directory. Filters the paths according to `path_filter`.
103 /// Logs the errors and ignores faulty entries/paths.
get_sorted_paths_from_dir( dir_path: &Path, path_filter: fn(&PathBuf) -> bool, ) -> Result<Vec<PathBuf>>104 fn get_sorted_paths_from_dir(
105     dir_path: &Path,
106     path_filter: fn(&PathBuf) -> bool,
107 ) -> Result<Vec<PathBuf>> {
108     let mut sorted_paths: Vec<PathBuf> = fs::read_dir(dir_path)
109         .with_context(|| format!("{ERR_READ_DIR} {:?}", dir_path))?
110         .filter_map(|entry| {
111             entry
112                 .inspect_err(|err| warn!("{ERR_DIR_ENTRY_ACCESS} {:?}: {err}", dir_path))
113                 .map(|entry| entry.path())
114                 .ok()
115         })
116         .filter(path_filter)
117         .collect();
118     sorted_paths.sort();
119     Ok(sorted_paths)
120 }
121 
122 /// Returns the rightmost element of a path.
get_path_basename(path: &Path) -> Result<&str>123 fn get_path_basename(path: &Path) -> Result<&str> {
124     let basename = path
125         .file_name()
126         .with_context(|| format!("{ERR_FILE_NAME_FROM_PATH} {:?}", path))?
127         .to_str()
128         .with_context(|| format!("{ERR_FILE_NAME_UTF_INVALID} {:?}", path))?;
129     Ok(basename)
130 }
131 
132 /// Decodes and prints VDOs from the files specified relative to `identity_path_buf` path.
133 /// * `filename_vdo_fields_arr` - Is an array of tuples containing the name of the file where the VDO can be found and the VDO description array (`&[VdoField]`).
print_decoded_vdos_from_files<W: Write>( identity_path_buf: &mut PathBuf, filename_vdo_fields_arr: &[(&str, &[VdoField])], out_writer: &mut OutputWriter<W>, )134 pub fn print_decoded_vdos_from_files<W: Write>(
135     identity_path_buf: &mut PathBuf,
136     filename_vdo_fields_arr: &[(&str, &[VdoField])],
137     out_writer: &mut OutputWriter<W>,
138 ) {
139     for (filename, vdo_fields) in filename_vdo_fields_arr {
140         identity_path_buf.push(filename);
141         let _ = print_vdo(identity_path_buf, vdo_fields, out_writer)
142             .inspect_err(|err| warn!("{:#}", err));
143         identity_path_buf.pop();
144     }
145 }
146 
147 /// Prints the identity information of a USB device.
148 /// This function reads and decodes the identity descriptors of a USB device,
149 /// including its ID header, certification status, and product information.
150 /// The output is formatted and written using the provided `OutputWriter`.
151 /// # Arguments
152 /// * `dev_path` - A reference to the Path of the device's directory.
153 /// * `out_writer` - A mutable reference to an OutputWriter for writing the output.
154 /// # Returns
155 /// * `Result<()>` - A Result indicating success or failure of the printing operation.
print_identity<W: Write>(dev_path: &Path, out_writer: &mut OutputWriter<W>) -> Result<()>156 pub fn print_identity<W: Write>(dev_path: &Path, out_writer: &mut OutputWriter<W>) -> Result<()> {
157     out_writer.print_str_indent("identity").inspect_err(|err| warn!("{:#}", err)).ok();
158 
159     out_writer.indent += INDENT_STEP;
160 
161     let filename_vdo_fields_arr: &[(&str, &[VdoField]); 3] = match get_pd_revision(dev_path) {
162         PdRev::Pd20 => &[
163             ("id_header", pd20_data::ID_HEADER_VDO),
164             ("cert_stat", pd20_data::CERT_STAT_VDO),
165             ("product", pd20_data::PRODUCT_VDO),
166         ],
167         PdRev::Pd30 => &[
168             ("id_header", pd30_data::ID_HEADER_VDO),
169             ("cert_stat", pd30_data::CERT_STAT_VDO),
170             ("product", pd30_data::PRODUCT_VDO),
171         ],
172         PdRev::Pd31 => &[
173             ("id_header", pd31_data::ID_HEADER_VDO),
174             ("cert_stat", pd31_data::CERT_STAT_VDO),
175             ("product", pd31_data::PRODUCT_VDO),
176         ],
177         PdRev::None => &[("id_header", &[]), ("cert_stat", &[]), ("product", &[])],
178     };
179     print_decoded_vdos_from_files(
180         &mut dev_path.join("identity"),
181         filename_vdo_fields_arr,
182         out_writer,
183     );
184     out_writer.indent -= INDENT_STEP;
185 
186     Ok(())
187 }
188 
189 /// Prints the contents of an identity directory including VDO fields which are determined by product type.
print_partner_identity<W: Write>( partner_path: &Path, out_writer: &mut OutputWriter<W>, ) -> Result<()>190 pub fn print_partner_identity<W: Write>(
191     partner_path: &Path,
192     out_writer: &mut OutputWriter<W>,
193 ) -> Result<()> {
194     let mut identity_path_buf: PathBuf = partner_path.join("identity");
195     if !(identity_path_buf.is_dir() | identity_path_buf.is_symlink()) {
196         bail!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", identity_path_buf);
197     }
198 
199     let _ = print_identity(partner_path, out_writer).inspect_err(|err| warn!("{:#}", err));
200 
201     out_writer.indent += INDENT_STEP;
202 
203     let pd_rev = get_pd_revision(partner_path);
204 
205     let vdo_fields_arr: [&[VdoField]; 3] = match get_partner_product_type(partner_path, pd_rev) {
206         ProductType::Ama(PdRev::Pd20) => [pd20_data::AMA_VDO, &[], &[]],
207         ProductType::Vpd(PdRev::Pd30) => [pd30_data::VPD_VDO, &[], &[]],
208         ProductType::Ama(PdRev::Pd30) => [pd30_data::AMA_VDO, &[], &[]],
209         ProductType::Port((PortType::Ufp, PdRev::Pd30)) => {
210             [pd30_data::UFP_VDO1, pd30_data::UFP_VDO2, &[]]
211         }
212         ProductType::Port((PortType::Dfp, PdRev::Pd30)) => [pd30_data::DFP_VDO, &[], &[]],
213         ProductType::Port((PortType::Drd, PdRev::Pd30)) => {
214             [pd30_data::UFP_VDO1, pd30_data::UFP_VDO2, pd30_data::DFP_VDO]
215         }
216         ProductType::Port((PortType::Ufp, PdRev::Pd31)) => [pd31_data::UFP_VDO, &[], &[]],
217         ProductType::Port((PortType::Dfp, PdRev::Pd31)) => [pd31_data::DFP_VDO, &[], &[]],
218         ProductType::Port((PortType::Drd, PdRev::Pd31)) => {
219             [pd31_data::UFP_VDO, &[], pd31_data::DFP_VDO]
220         }
221         _ => [&[], &[], &[]],
222     };
223     let filenames = ["product_type_vdo1", "product_type_vdo2", "product_type_vdo3"];
224     let filename_vdo_fields_arr: Vec<(&str, &[VdoField])> =
225         filenames.into_iter().zip(vdo_fields_arr).collect();
226 
227     print_decoded_vdos_from_files(&mut identity_path_buf, &filename_vdo_fields_arr, out_writer);
228 
229     out_writer.indent -= INDENT_STEP;
230 
231     Ok(())
232 }
233 
234 /// Reads the usb_power_delivery_revision file in a given directory
235 /// and returns a PdRev enum noting the supported PD Revision.
get_pd_revision(dir_path: &Path) -> PdRev236 pub fn get_pd_revision(dir_path: &Path) -> PdRev {
237     let pd_path: PathBuf = dir_path.join("usb_power_delivery_revision");
238     let pd_revision_str = fs::read_to_string(&pd_path)
239         .inspect_err(|err| warn!("{ERR_READ_FILE} {:?} {:#}", &pd_path, err))
240         .unwrap_or_default();
241     match pd_revision_str.trim_ascii() {
242         "2.0" => PdRev::Pd20,
243         "3.0" => PdRev::Pd30,
244         "3.1" => PdRev::Pd31,
245         _ => PdRev::None,
246     }
247 }
248 
249 /// Uses the id_header VDO and USB PD revision to decode what type of device is being parsed.
250 /// In case of failure returns ProductType::Other.
get_partner_product_type(partner_dir: &Path, pd_rev: PdRev) -> ProductType251 pub fn get_partner_product_type(partner_dir: &Path, pd_rev: PdRev) -> ProductType {
252     let id_header_vdo_path: PathBuf = partner_dir.join("identity").join("id_header");
253 
254     let id_header_vdo = match read_vdo(&id_header_vdo_path) {
255         Ok(vdo) => vdo,
256         Err(err) => {
257             warn!("{:#}", err);
258             return ProductType::Other;
259         }
260     };
261 
262     match pd_rev {
263         PdRev::Pd20 => {
264             // Alternate Mode Adapter (AMA) is the only partner product type in the
265             // USB PD 2.0 specification.
266             match id_header_vdo & UFP_PRODUCT_TYPE_MASK {
267                 pd20_data::AMA_COMP => ProductType::Ama(PdRev::Pd20),
268                 _ => ProductType::Other,
269             }
270         }
271         PdRev::Pd30 => {
272             // In USB PD 3.0 a partner can be an upstream facing port (UFP),
273             // downstream facing port (DFP), or a dual-role data port (DRD).
274             // Information about UFP/DFP are in different fields, so they are checked
275             // separately then compared to determine a partner's product type.
276             // Separate from UFP/DFP, they can support AMA/VPD as a UFP type.
277 
278             let ufp_supported = match id_header_vdo & UFP_PRODUCT_TYPE_MASK {
279                 pd30_data::HUB_COMP => true,
280                 pd30_data::PERIPHERAL_COMP => true,
281                 pd30_data::AMA_COMP => return ProductType::Ama(PdRev::Pd30),
282                 pd30_data::VPD_COMP => return ProductType::Vpd(PdRev::Pd30),
283                 _ => false,
284             };
285 
286             let dfp_supported = matches!(
287                 id_header_vdo & DFP_PRODUCT_TYPE_MASK,
288                 pd30_data::DFP_HUB_COMP | pd30_data::DFP_HOST_COMP | pd30_data::POWER_BRICK_COMP
289             );
290 
291             match (ufp_supported, dfp_supported) {
292                 (true, true) => ProductType::Port((PortType::Drd, PdRev::Pd30)),
293                 (true, false) => ProductType::Port((PortType::Ufp, PdRev::Pd30)),
294                 (false, true) => ProductType::Port((PortType::Dfp, PdRev::Pd30)),
295                 _ => ProductType::Other,
296             }
297         }
298         PdRev::Pd31 => {
299             // Similar to USB PD 3.0, USB PD 3.1 can have a partner which is both UFP
300             // and DFP (DRD). AMA product type is no longer present in PD 3.1.
301             let ufp_supported = matches!(
302                 id_header_vdo & UFP_PRODUCT_TYPE_MASK,
303                 pd31_data::HUB_COMP | pd31_data::PERIPHERAL_COMP
304             );
305 
306             let dfp_supported = matches!(
307                 id_header_vdo & DFP_PRODUCT_TYPE_MASK,
308                 pd31_data::DFP_HOST_COMP | pd31_data::DFP_HUB_COMP | pd31_data::POWER_BRICK_COMP
309             );
310 
311             match (ufp_supported, dfp_supported) {
312                 (true, true) => ProductType::Port((PortType::Drd, PdRev::Pd31)),
313                 (true, false) => ProductType::Port((PortType::Ufp, PdRev::Pd31)),
314                 (false, true) => ProductType::Port((PortType::Dfp, PdRev::Pd31)),
315                 _ => ProductType::Other,
316             }
317         }
318         _ => ProductType::Other,
319     }
320 }
321 
322 /// Reads a vdo value from a text file and converts it to a u32 variable. Then, prints out the values of each field according to the vdo_description.
print_vdo<W: Write>( vdo_path: &Path, vdo_description: &[VdoField], out_writer: &mut OutputWriter<W>, ) -> Result<()>323 pub fn print_vdo<W: Write>(
324     vdo_path: &Path,
325     vdo_description: &[VdoField],
326     out_writer: &mut OutputWriter<W>,
327 ) -> Result<()> {
328     let vdo = read_vdo(vdo_path)?;
329     let vdo_str = format!("{}: 0x{:x}", get_path_basename(vdo_path)?, vdo);
330     let _ = out_writer.print_str_indent(&vdo_str).inspect_err(|err| warn!("{:#}", err));
331     out_writer.indent += INDENT_STEP;
332     for vdo_field in vdo_description {
333         let _ = out_writer
334             .print_str_indent(&vdo_field.decode_vdo(vdo))
335             .inspect_err(|err| warn!("{:#}", err)); // log error but continue iterating.
336     }
337     out_writer.indent -= INDENT_STEP;
338     Ok(())
339 }
340 
341 /// Reads a file containing a 32 bit VDO value and loads it into a u32.
read_vdo(vdo_path: &Path) -> Result<u32>342 pub fn read_vdo(vdo_path: &Path) -> Result<u32> {
343     let vdo_str =
344         fs::read_to_string(vdo_path).with_context(|| format!("{ERR_READ_FILE} {:?}", vdo_path))?;
345     let vdo_str = vdo_str.trim_ascii();
346     let vdo: u32 =
347         u32::from_str_radix(vdo_str.trim_start_matches("0x"), 16).with_context(|| {
348             format!("Cannot convert string read from path: {:?} to u32: {vdo_str}", vdo_path)
349         })?;
350     Ok(vdo)
351 }
352 
353 // Prints the immediate files in an alternate mode directory, then prints the files in each mode subdirectory.
print_alt_mode<W: Write>(alt_mode_dir_path: &Path, out_writer: &mut OutputWriter<W>)354 pub fn print_alt_mode<W: Write>(alt_mode_dir_path: &Path, out_writer: &mut OutputWriter<W>) {
355     if !(alt_mode_dir_path.is_dir() | alt_mode_dir_path.is_symlink()) {
356         warn!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", alt_mode_dir_path);
357         return;
358     }
359 
360     let _ = out_writer.print_dir_files(alt_mode_dir_path).inspect_err(|err| warn!("{:#}", err));
361     out_writer.indent += INDENT_STEP;
362     let _ = parse_dirs_and_execute(
363         alt_mode_dir_path,
364         out_writer,
365         MODE_REGEX,
366         |path: &Path, out_writer: &mut OutputWriter<W>| {
367             let _ = out_writer.print_dir_files(path).inspect_err(|err| warn!("{:#}", err));
368         },
369     )
370     .inspect_err(|err| warn!("{:#}", err));
371     out_writer.indent -= INDENT_STEP;
372 }
373 
374 // Prints detailed information about the PDOs given at capabilities_dir_path, including available voltages and currents.
print_pdo_capabilities<W: Write>(capabilities: &Path, out_writer: &mut OutputWriter<W>)375 pub fn print_pdo_capabilities<W: Write>(capabilities: &Path, out_writer: &mut OutputWriter<W>) {
376     let _ = out_writer.print_dir_files(capabilities).inspect_err(|err| warn!("{:#}", err));
377     out_writer.indent += INDENT_STEP;
378     let _ = parse_dirs_and_execute(
379         capabilities,
380         out_writer,
381         PDO_TYPE_REGEX,
382         |path: &Path, out_writer: &mut OutputWriter<W>| {
383             let _ = out_writer.print_dir_files(path).inspect_err(|err| warn!("{:#}", err));
384         },
385     )
386     .inspect_err(|err| warn!("{:#}", err));
387     out_writer.indent -= INDENT_STEP;
388 }
389 
390 // Prints the immediate files in a PDO data directory, then call
391 // print_pdo_capabilities to print more detailed PDO information.
print_pdos<W: Write>(pdo_dir_path: &Path, out_writer: &mut OutputWriter<W>)392 pub fn print_pdos<W: Write>(pdo_dir_path: &Path, out_writer: &mut OutputWriter<W>) {
393     if !(pdo_dir_path.is_dir() | pdo_dir_path.is_symlink()) {
394         warn!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", pdo_dir_path);
395         return;
396     }
397 
398     let _ = out_writer.print_dir_files(pdo_dir_path).inspect_err(|err| warn!("{:#}", err));
399     out_writer.indent += INDENT_STEP;
400     let _ = parse_dirs_and_execute(
401         pdo_dir_path,
402         out_writer,
403         PDO_CAPABILITIES_REGEX,
404         print_pdo_capabilities,
405     )
406     .inspect_err(|err| warn!("{:#}", err));
407     out_writer.indent -= INDENT_STEP;
408 }
409 
410 /// Prints the immediate information in the partner directory, then prints identity, alternate mode, and power data objects (PDO) information.
print_partner<W: Write>(port_path: &Path, out_writer: &mut OutputWriter<W>) -> Result<()>411 pub fn print_partner<W: Write>(port_path: &Path, out_writer: &mut OutputWriter<W>) -> Result<()> {
412     let partner_dir_name = [get_path_basename(port_path)?, "-partner"].concat();
413     let partner_path_buf: PathBuf = port_path.join(partner_dir_name);
414 
415     if !(partner_path_buf.is_dir() | partner_path_buf.is_symlink()) {
416         bail!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", partner_path_buf);
417     }
418 
419     let _ = out_writer.print_dir_files(&partner_path_buf).inspect_err(|err| warn!("{:#}", err));
420 
421     out_writer.indent += INDENT_STEP;
422 
423     let _ =
424         print_partner_identity(&partner_path_buf, out_writer).inspect_err(|err| warn!("{:#}", err));
425 
426     let _ = parse_dirs_and_execute(
427         &partner_path_buf,
428         out_writer,
429         PARTNER_ALT_MODE_REGEX,
430         print_alt_mode,
431     )
432     .inspect_err(|err| warn!("{:#}", err));
433 
434     let _ = parse_dirs_and_execute(&partner_path_buf, out_writer, PARTNER_PDO_REGEX, print_pdos)
435         .inspect_err(|err| warn!("{:#}", err));
436 
437     out_writer.indent -= INDENT_STEP;
438     Ok(())
439 }
440 
441 /// Determines the product type of a USB cable by reading its identity descriptors.
442 /// This function reads the "id_header" file within the "identity" directory of the cable's path
443 /// to determine its product type based on the USB PD revision and product mask.
444 /// # Arguments
445 /// * `dir` - A reference to the Path of the cable's directory.
446 /// # Returns
447 /// * `ProductType` - The determined product type of the cable.
get_cable_product_type(dir: &Path) -> ProductType448 pub fn get_cable_product_type(dir: &Path) -> ProductType {
449     let id_header = match read_vdo(&dir.join("identity").join("id_header")) {
450         Err(err) => {
451             warn!("get cable type failed to read vdo: {:#}", err);
452             return ProductType::Other;
453         }
454         Ok(id_header) => id_header,
455     };
456 
457     let pd = get_pd_revision(dir);
458     let prod_mask = id_header & UFP_PRODUCT_TYPE_MASK;
459 
460     // All USB PD 3.1/3.0/2.0 support passive cables.
461     if pd == PdRev::Pd20 && prod_mask == pd20_data::PASSIVE_CABLE_COMP
462         || pd == PdRev::Pd30 && prod_mask == pd30_data::PASSIVE_CABLE_COMP
463         || pd == PdRev::Pd31 && prod_mask == pd31_data::PASSIVE_CABLE_COMP
464     {
465         return ProductType::Cable((CableType::Passive, pd));
466     }
467 
468     // All USB PD 3.1/3.0/2.0 support active cables.
469     if pd == PdRev::Pd20 && prod_mask == pd20_data::ACTIVE_CABLE_COMP
470         || pd == PdRev::Pd30 && prod_mask == pd30_data::ACTIVE_CABLE_COMP
471         || pd == PdRev::Pd31 && prod_mask == pd31_data::ACTIVE_CABLE_COMP
472     {
473         return ProductType::Cable((CableType::Active, pd));
474     }
475 
476     // USB PD 3.1 also supports Vconn Powered Devices (VPD).
477     if pd == PdRev::Pd31 && prod_mask == pd31_data::VPD_COMP {
478         return ProductType::Cable((CableType::Vpd, pd));
479     }
480 
481     ProductType::Other
482 }
483 
484 /// Prints the identity information of a USB cable.
485 /// This function prints the identity information of a USB cable, including its product type and
486 /// decoded VDOs (Vendor-Defined Objects) based on its PD revision.
487 /// # Arguments
488 /// * `cable` - A reference to the Path of the cable's directory.
489 /// * `out_writer` - A mutable reference to an OutputWriter for writing the output.
print_cable_identity<W: Write>(cable: &Path, out_writer: &mut OutputWriter<W>)490 pub fn print_cable_identity<W: Write>(cable: &Path, out_writer: &mut OutputWriter<W>) {
491     let mut identity = cable.join("identity");
492     if !(identity.is_dir() | identity.is_symlink()) {
493         warn!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", identity);
494         return;
495     }
496 
497     let _ = print_identity(cable, out_writer).inspect_err(|err| warn!("{:#}", err));
498 
499     out_writer.indent += INDENT_STEP;
500 
501     let vdos: [&[VdoField]; 3] = match get_cable_product_type(cable) {
502         ProductType::Cable(cable_type) => match cable_type {
503             (CableType::Passive, PdRev::Pd20) => [pd20_data::PASSIVE_VDO, &[], &[]],
504             (CableType::Active, PdRev::Pd20) => [pd20_data::ACTIVE_VDO, &[], &[]],
505             (CableType::Passive, PdRev::Pd30) => [pd30_data::PASSIVE_VDO, &[], &[]],
506             (CableType::Active, PdRev::Pd30) => {
507                 [pd30_data::ACTIVE_VDO1, pd30_data::ACTIVE_VDO2, &[]]
508             }
509             (CableType::Passive, PdRev::Pd31) => [pd31_data::PASSIVE_VDO, &[], &[]],
510             (CableType::Active, PdRev::Pd31) => {
511                 [pd31_data::ACTIVE_VDO1, pd31_data::ACTIVE_VDO2, &[]]
512             }
513             (CableType::Vpd, PdRev::Pd31) => [pd31_data::VPD_VDO, &[], &[]],
514             _ => [&[], &[], &[]],
515         },
516         _ => [&[], &[], &[]],
517     };
518 
519     let descriptions = ["product_type_vdo1", "product_type_vdo2", "product_type_vdo3"];
520     let to_print: Vec<(&str, &[VdoField])> = descriptions.into_iter().zip(vdos).collect();
521 
522     print_decoded_vdos_from_files(&mut identity, &to_print, out_writer);
523 
524     out_writer.indent -= INDENT_STEP;
525 }
526 
527 /// Prints information about a USB cable plug.
528 /// This function prints information about a USB cable plug, including its alternate modes.
529 /// # Arguments
530 /// * `plug` - A reference to the Path of the plug's directory.
531 /// * `out_writer` - A mutable reference to an OutputWriter for writing the output.
print_plug_info<W: Write>(plug: &Path, out_writer: &mut OutputWriter<W>)532 pub fn print_plug_info<W: Write>(plug: &Path, out_writer: &mut OutputWriter<W>) {
533     if !(plug.is_dir() | plug.is_symlink()) {
534         warn!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", plug);
535         return;
536     }
537 
538     let _ = out_writer.print_dir_files(plug);
539     out_writer.indent += INDENT_STEP;
540     let _ = parse_dirs_and_execute(plug, out_writer, PLUG_ALT_MODE_REGEX, print_alt_mode)
541         .inspect_err(|err| warn!("{:#}", err));
542     out_writer.indent -= INDENT_STEP;
543 }
544 
545 /// Prints information about a USB cable connected to a port.
546 /// This function prints information about a USB cable connected to a port, including its identity,
547 /// plug information, and alternate modes.
548 /// # Arguments
549 /// * `port` - A reference to the Path of the port's directory.
550 /// * `out_writer` - A mutable reference to an OutputWriter for writing the output.
551 /// # Returns
552 /// * `Result<()>` - A Result indicating success or failure.
print_cable<W: Write>(port: &Path, out_writer: &mut OutputWriter<W>) -> Result<()>553 pub fn print_cable<W: Write>(port: &Path, out_writer: &mut OutputWriter<W>) -> Result<()> {
554     let cable_path = format!("{}-cable", get_path_basename(port)?);
555     let cable_dir = port.join(cable_path);
556 
557     if !(cable_dir.is_dir() | cable_dir.is_symlink()) {
558         bail!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", cable_dir);
559     }
560 
561     out_writer.print_dir_files(&cable_dir).inspect_err(|err| warn!("{:#}", err)).ok();
562 
563     out_writer.indent += INDENT_STEP;
564     print_cable_identity(&cable_dir, out_writer);
565     let _ = parse_dirs_and_execute(&cable_dir, out_writer, PLUG_REGEX, print_plug_info)
566         .inspect_err(|err| warn!("{:#}", err));
567     out_writer.indent -= INDENT_STEP;
568 
569     Ok(())
570 }
571 
572 /// Prints the panel and horizontal_position in the physical_location directory of a port.
print_physical_location<W: Write>( port_path: &Path, out_writer: &mut OutputWriter<W>, ) -> Result<()>573 pub fn print_physical_location<W: Write>(
574     port_path: &Path,
575     out_writer: &mut OutputWriter<W>,
576 ) -> Result<()> {
577     let physical_location_dir = port_path.join("physical_location");
578     if !(physical_location_dir.is_dir() | physical_location_dir.is_symlink()) {
579         bail!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", physical_location_dir);
580     }
581     out_writer.print_str_indent("physical_location")?;
582 
583     out_writer.indent += INDENT_STEP;
584     out_writer.print_file_formatted(&physical_location_dir.join("panel"))?;
585     out_writer.print_file_formatted(&physical_location_dir.join("horizontal_position"))?;
586     out_writer.indent -= INDENT_STEP;
587 
588     Ok(())
589 }
590 
591 // Prints the `busnum`, `devnum`, `devpath` in the usb device directory, which are minimal
592 // info needed to map to corresponding peripheral.
print_usb_device_info<W: Write>(usb_device: &Path, out_writer: &mut OutputWriter<W>)593 pub fn print_usb_device_info<W: Write>(usb_device: &Path, out_writer: &mut OutputWriter<W>) {
594     out_writer.print_str_indent("usb_device").inspect_err(|err| warn!("{:#}", err)).ok();
595 
596     out_writer.indent += INDENT_STEP;
597     out_writer
598         .print_file_formatted(&usb_device.join("busnum"))
599         .inspect_err(|err| warn!("{:#}", err))
600         .ok();
601     out_writer
602         .print_file_formatted(&usb_device.join("devnum"))
603         .inspect_err(|err| warn!("{:#}", err))
604         .ok();
605     out_writer
606         .print_file_formatted(&usb_device.join("devpath"))
607         .inspect_err(|err| warn!("{:#}", err))
608         .ok();
609     parse_dirs_and_execute(usb_device, out_writer, USB_DEVICE_REGEX, print_usb_device_info)
610         .inspect_err(|err| warn!("{:#}", err))
611         .ok();
612     out_writer.indent -= INDENT_STEP;
613 }
614 
615 // Finds and prints information about the usb device in the usb port directory.
print_usb_device<W: Write>(usb_port: &Path, out_writer: &mut OutputWriter<W>)616 pub fn print_usb_device<W: Write>(usb_port: &Path, out_writer: &mut OutputWriter<W>) {
617     let usb_device_dir = usb_port.join("device");
618     if !(usb_device_dir.is_dir() | usb_device_dir.is_symlink()) {
619         warn!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", usb_device_dir);
620         return;
621     }
622 
623     print_usb_device_info(&usb_device_dir, out_writer);
624 }
625 
626 /// Finds the type-c port's linked usb ports and print relevant usb subsystem information.
print_usb_subsystem<W: Write>( port_path: &Path, out_writer: &mut OutputWriter<W>, ) -> Result<()>627 pub fn print_usb_subsystem<W: Write>(
628     port_path: &Path,
629     out_writer: &mut OutputWriter<W>,
630 ) -> Result<()> {
631     let _ = parse_dirs_and_execute(port_path, out_writer, USB_PORT_REGEX, print_usb_device);
632     Ok(())
633 }
634 
635 /// Prints `connector_id` and `status` in the drm_connector directory which is a symlink
636 /// to the corresponding DP connector.
print_drm_subsystem<W: Write>( port_path: &Path, out_writer: &mut OutputWriter<W>, ) -> Result<()>637 pub fn print_drm_subsystem<W: Write>(
638     port_path: &Path,
639     out_writer: &mut OutputWriter<W>,
640 ) -> Result<()> {
641     let drm_path = port_path.join("drm_connector");
642     if !(drm_path.is_dir() | drm_path.is_symlink()) {
643         bail!("{ERR_PATH_NOT_DIR_OR_SYMLINK}: {:?}", drm_path);
644     }
645     out_writer.print_str_indent("dp_connector")?;
646 
647     out_writer.indent += INDENT_STEP;
648     out_writer.print_file_formatted(&drm_path.join("connector_id"))?;
649     out_writer.print_file_formatted(&drm_path.join("status"))?;
650     out_writer.indent -= INDENT_STEP;
651 
652     Ok(())
653 }
654 
655 /// Prints relevant type-c connector class information for the port located at the sysfs path "port_path".
print_port_info<W: Write>(port_path: &Path, out_writer: &mut OutputWriter<W>)656 pub fn print_port_info<W: Write>(port_path: &Path, out_writer: &mut OutputWriter<W>) {
657     let _ = out_writer.print_dir_files(port_path).inspect_err(|err| warn!("{:#}", err));
658     out_writer.indent += INDENT_STEP;
659     let _ = print_partner(port_path, out_writer).inspect_err(|err| warn!("{:#}", err));
660     print_cable(port_path, out_writer).inspect_err(|err| warn!("{:#}", err)).ok();
661     print_physical_location(port_path, out_writer).inspect_err(|err| warn!("{:#}", err)).ok();
662     print_usb_subsystem(port_path, out_writer).inspect_err(|err| warn!("{:#}", err)).ok();
663     print_drm_subsystem(port_path, out_writer).inspect_err(|err| warn!("{:#}", err)).ok();
664     out_writer.indent -= INDENT_STEP;
665 }
666 
667 /// Looks at subdirectories of a given directory and executes a passed function on directories matching a given regular expression.
668 /// Directories are sorted alphabetically by path to ensure consistency in the order of processing.
parse_dirs_and_execute<W: Write>( dir_path: &Path, out_writer: &mut OutputWriter<W>, regex: &str, func: fn(&Path, &mut OutputWriter<W>), ) -> Result<()>669 pub fn parse_dirs_and_execute<W: Write>(
670     dir_path: &Path,
671     out_writer: &mut OutputWriter<W>,
672     regex: &str,
673     func: fn(&Path, &mut OutputWriter<W>),
674 ) -> Result<()> {
675     let re = Regex::new(regex).unwrap();
676     for path in get_sorted_paths_from_dir(dir_path, |path| path.is_dir() | path.is_symlink())? {
677         // Assumption: all the symlinks created by the Type-C connector class point to directories.
678         get_path_basename(&path)
679             .inspect_err(|err| warn!("{:#}", err))
680             .is_ok_and(|file_name| re.is_match(file_name))
681             .then(|| func(&path, out_writer));
682     }
683     Ok(())
684 }
685