srcsrv-0.2.2/.cargo_vcs_info.json0000644000000001120000000000100123510ustar { "git": { "sha1": "2ed138dc5a08fa8cd4841b5e0911a541f3de0bd1" } } srcsrv-0.2.2/.gitignore000064400000000000000000000000350072674642500131650ustar 00000000000000/target Cargo.lock .DS_Store srcsrv-0.2.2/Cargo.toml0000644000000021130000000000100103520ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "srcsrv" version = "0.2.2" authors = ["Markus Stange "] exclude = ["/.github", "/tests"] description = "Interpret the contents of a srcsrv stream from a pdb file (Microsoft Source Server)." readme = "README.md" keywords = ["PDB", "pdbstr", "symbols", "source", "windows"] categories = ["development-tools::debugging"] license = "MIT/Apache-2.0" repository = "https://github.com/mstange/srcsrv" [dependencies.memchr] version = "2.4.1" [dependencies.thiserror] version = "1.0" [dev-dependencies.pdb] version = "0.7.0" srcsrv-0.2.2/Cargo.toml.orig000064400000000000000000000010300072674642500140600ustar 00000000000000[package] name = "srcsrv" version = "0.2.2" edition = "2018" authors = ["Markus Stange "] description = "Interpret the contents of a srcsrv stream from a pdb file (Microsoft Source Server)." categories = ["development-tools::debugging"] keywords = ["PDB", "pdbstr", "symbols", "source", "windows"] license = "MIT/Apache-2.0" readme = "README.md" repository = "https://github.com/mstange/srcsrv" exclude = ["/.github", "/tests"] [dependencies] memchr = "2.4.1" thiserror = "1.0" [dev-dependencies] pdb = "0.7.0" srcsrv-0.2.2/LICENSE-APACHE000064400000000000000000000251370072674642500131330ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. srcsrv-0.2.2/LICENSE-MIT000064400000000000000000000020700072674642500126320ustar 00000000000000Copyright (c) 2018 Markus Stange Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. srcsrv-0.2.2/README.md000064400000000000000000000041540072674642500124620ustar 00000000000000[![crates.io page](https://img.shields.io/crates/v/srcsrv.svg)](https://crates.io/crates/srcsrv) [![docs.rs page](https://docs.rs/srcsrv/badge.svg)](https://docs.rs/srcsrv/) # srcsrv Parse a `srcsrv` stream from a Windows PDB file and look up file paths to see how the source for these paths can be obtained: - Either by downloading the file from a URL directly ([`SourceRetrievalMethod::Download`](https://docs.rs/srcsrv/0.2.1/srcsrv/enum.SourceRetrievalMethod.html#variant.Download)), - or by executing a command, which will create the file at a certain path ([`SourceRetrievalMethod::ExecuteCommand`](https://docs.rs/srcsrv/0.2.1/srcsrv/enum.SourceRetrievalMethod.html#variant.ExecuteCommand)) ```rust use srcsrv::{SrcSrvStream, SourceRetrievalMethod}; if let Ok(srcsrv_stream) = pdb.named_stream(b"srcsrv") { let stream = SrcSrvStream::parse(srcsrv_stream.as_slice())?; let url = match stream.source_for_path( r#"C:\build\renderdoc\renderdoc\data\glsl\gl_texsample.h"#, r#"C:\Debugger\Cached Sources"#, )? { SourceRetrievalMethod::Download { url } => Some(url), _ => None, }; assert_eq!(url, Some("https://raw.githubusercontent.com/baldurk/renderdoc/v1.15/renderdoc/data/glsl/gl_texsample.h".to_string())); } ``` ## Further reading - [Source indexing for github projects](https://gist.github.com/baldurk/c6feb31b0305125c6d1a) - [Microsoft documentation: Overview](https://docs.microsoft.com/en-us/windows/win32/debug/source-server-and-source-indexing) - [Microsoft documentation: Language specification](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/language-specification-1) ## License Licensed under either of * Apache License, Version 2.0 ([`LICENSE-APACHE`](./LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([`LICENSE-MIT`](./LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. srcsrv-0.2.2/src/ast.rs000064400000000000000000000127100072674642500131240ustar 00000000000000use crate::errors::{EvalError, ParseError}; use std::result::Result; use memchr::{memchr, memchr2}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AstNode<'a> { /// String concatenation of the evaluated child nodes. Sequence(Vec>), /// A literal string. LiteralString(&'a str), /// Substitute with the value of the variable with this name. Variable(&'a str), /// Substitute with the value of the variable whose name is given by the /// value of the variable with this name. FnVar(Box>), /// Substitute with the string but with all slashes replaced by backslashes. FnBackslash(Box>), /// Substitute with the file name extracted from the path. FnFile(Box>), } impl<'a> AstNode<'a> { pub fn parse(s: &'a str) -> Result, ParseError> { if s.is_empty() { return Ok(AstNode::LiteralString("")); } let s = s.as_bytes(); let (node, _rest) = Self::parse_all(s, false)?; Ok(node) } fn parse_all(s: &'a [u8], nested: bool) -> Result<(AstNode<'a>, &'a [u8]), ParseError> { let (node, rest) = Self::parse_one(s, nested)?; if rest.is_empty() || (nested && rest[0] == b')') { return Ok((node, rest)); } let mut nodes = vec![node]; let mut rest = rest; loop { let (node, r) = Self::parse_one(rest, nested)?; nodes.push(node); rest = r; if rest.is_empty() || (nested && rest[0] == b')') { return Ok((AstNode::Sequence(nodes), rest)); } } } // s must not be empty fn parse_one(s: &'a [u8], nested: bool) -> Result<(AstNode<'a>, &'a [u8]), ParseError> { if s[0] != b'%' { // We have a literal at the beginning. let literal_end = if nested { memchr2(b'%', b')', s) } else { memchr(b'%', s) }; let literal_end = literal_end.unwrap_or(s.len()); let (literal, rest) = s.split_at(literal_end); let string = std::str::from_utf8(literal).map_err(|_| ParseError::InvalidUtf8)?; return Ok((AstNode::LiteralString(string), rest)); } // We start with a %. let s = &s[1..]; let second_percent_pos = memchr(b'%', s).ok_or(ParseError::MissingPercent)?; let rest = &s[second_percent_pos + 1..]; let var_name = std::str::from_utf8(&s[..second_percent_pos]).map_err(|_| ParseError::InvalidUtf8)?; match var_name.to_ascii_lowercase().as_str() { "fnvar" => { let (node, rest) = Self::try_parse_args(rest, "fnvar")?; Ok((AstNode::FnVar(Box::new(node)), rest)) } "fnbksl" => { let (node, rest) = Self::try_parse_args(rest, "fnbksl")?; Ok((AstNode::FnBackslash(Box::new(node)), rest)) } "fnfile" => { let (node, rest) = Self::try_parse_args(rest, "fnfile")?; Ok((AstNode::FnFile(Box::new(node)), rest)) } _ => Ok((AstNode::Variable(var_name), rest)), } } fn try_parse_args(s: &'a [u8], function: &str) -> Result<(AstNode<'a>, &'a [u8]), ParseError> { if s.is_empty() || s[0] != b'(' { return Err(ParseError::MissingOpeningParen(function.to_string())); } let (node, rest) = Self::parse_all(&s[1..], true)?; if rest.is_empty() || rest[0] != b')' { return Err(ParseError::MissingClosingParen(function.to_string())); } Ok((node, &rest[1..])) } pub fn eval(&self, f: &mut F) -> Result where F: FnMut(&str) -> Result, { match self { AstNode::Sequence(nodes) => { let values: Result, EvalError> = nodes.iter().map(|node| node.eval(f)).collect(); Ok(values?.join("")) } AstNode::LiteralString(s) => Ok(s.to_string()), AstNode::Variable(var_name) => f(var_name), AstNode::FnVar(node) => { let var_name = node.eval(f)?; f(&var_name) } AstNode::FnBackslash(node) => { let val = node.eval(f)?; Ok(val.replace('/', "\\")) } AstNode::FnFile(node) => { let val = node.eval(f)?; match val.rsplit_once('\\') { Some((_base, file)) => Ok(file.to_string()), None => Ok(val), } } } } } #[cfg(test)] mod tests { use crate::{AstNode, ParseError}; #[test] fn basic_parsing() -> Result<(), ParseError> { assert_eq!(AstNode::parse("hello")?, AstNode::LiteralString("hello")); assert_eq!( AstNode::parse("hello%world%")?, AstNode::Sequence(vec![ AstNode::LiteralString("hello"), AstNode::Variable("world") ]) ); assert_eq!( AstNode::parse("%hello%world")?, AstNode::Sequence(vec![ AstNode::Variable("hello"), AstNode::LiteralString("world") ]) ); assert_eq!( AstNode::parse("%fnfile%(world)")?, AstNode::FnFile(Box::new(AstNode::LiteralString("world"))) ); Ok(()) } } srcsrv-0.2.2/src/errors.rs000064400000000000000000000033770072674642500136620ustar 00000000000000/// An enum for errors that occur during stream parsing. #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum ParseError { #[error("The srcsrv stream is not valid utf-8.")] InvalidUtf8, #[error("The srcsrv stream ended unexpectedly.")] UnexpectedEof, #[error("Version {0} is not a recognized srcsrv stream version.")] UnrecognizedVersion(String), #[error("The VERSION ini variable is missing.")] MissingVersion, #[error("Could not find the ini section in the srcsrv stream.")] MissingIniSection, #[error("Could not find the variables section in the srcsrv stream.")] MissingVariablesSection, #[error("The SRCSRVTRG field was missing. This is a required field.")] MissingSrcSrvTrgField, #[error("Could not find the source files section in the srcsrv stream.")] MissingSourceFilesSection, #[error("Could not find the end marker line in theh srcsrv stream.")] MissingTerminationLine, #[error("Missing = in a variable line in the srcsrv stream.")] MissingEquals, #[error("Missing closing % in srcsrv variable use.")] MissingPercent, #[error("Expected ( after {0} function in srcsrv variable.")] MissingOpeningParen(String), #[error("Could not find closing ) for {0} function in srcsrv variable.")] MissingClosingParen(String), } /// An enum for errors that can occur when looking up the SourceRetrievalMethod /// for a file, and when evaluating the variables. #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum EvalError { #[error("Encountered recursion while evaluating srcsrv variable {0}.")] Recursion(String), #[error("Could not resolve srcsrv variable name {0}.")] UnknownVariable(String), } srcsrv-0.2.2/src/lib.rs000064400000000000000000000610320072674642500131040ustar 00000000000000//! Parse a `srcsrv` stream from a Windows PDB file and look up file //! paths to see how the source for these paths can be obtained: //! //! - Either by downloading the file from a URL directly ([`SourceRetrievalMethod::Download`]), //! - or by executing a command, which will create the file at a certain path ([`SourceRetrievalMethod::ExecuteCommand`]) //! //! ``` //! use srcsrv::{SrcSrvStream, SourceRetrievalMethod}; //! //! # fn wrapper<'s, S: pdb::Source<'s> + 's>(pdb: &mut pdb::PDB<'s, S>) -> std::result::Result<(), Box> { //! if let Ok(srcsrv_stream) = pdb.named_stream(b"srcsrv") { //! let stream = SrcSrvStream::parse(srcsrv_stream.as_slice())?; //! let url = match stream.source_for_path( //! r#"C:\build\renderdoc\renderdoc\data\glsl\gl_texsample.h"#, //! r#"C:\Debugger\Cached Sources"#, //! )? { //! Some(SourceRetrievalMethod::Download { url }) => Some(url), //! _ => None, //! }; //! assert_eq!(url, Some("https://raw.githubusercontent.com/baldurk/renderdoc/v1.15/renderdoc/data/glsl/gl_texsample.h".to_string())); //! } //! # Ok(()) //! # } //! ``` use std::collections::{HashMap, HashSet}; use std::result::Result; mod ast; mod errors; use ast::AstNode; pub use errors::{EvalError, ParseError}; /// A map of variables with their evaluated values. pub type EvalVarMap = HashMap; /// Describes how the source file can be obtained. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SourceRetrievalMethod { /// The source can be downloaded from the web, at the given URL. Download { url: String }, /// Evaluating the given command on the Windows Command shell with the given /// environment variables will create the source file at `target_path`. ExecuteCommand { /// The command to execute. command: String, /// The environment veriables to set during command execution. env: HashMap, /// An optional version control string. version_ctrl: Option, /// The path at which the extracted file will appear once the command has run. target_path: String, /// An optional string which identifies files that use the same version control /// system. Used for error persistence. /// If a file encounters an error during command execution, and the command output /// matches one of the strings in [`SrcSrvStream::error_persistence_command_output_strings()`], /// execution of the command should be skipped for all future entries with the same /// `error_persistence_version_control` value. /// See . error_persistence_version_control: Option, }, /// Grab bag for other cases. Please file issues about any extra cases you need. Other { raw_var_values: EvalVarMap }, } /// A parsed representation of the `srcsrv` stream from a PDB file. pub struct SrcSrvStream<'a> { /// 1, 2 or 3, based on the VERSION={} field version: u8, /// lowercase field name -> field value ini_fields: HashMap, /// lowercase field name -> (raw field value, parsed field value ast node) var_fields: HashMap)>, /// lowercase original path -> [var1, ..., var10] source_file_entries: HashMap>, } impl<'a> SrcSrvStream<'a> { /// Parse the `srcsrv` stream. The stream bytes can be obtained with the help of /// the [`PDB::named_stream` method from the `pdb` crate](https://docs.rs/pdb/0.7.0/pdb/struct.PDB.html#method.named_stream). /// /// ``` /// use srcsrv::SrcSrvStream; /// /// # fn wrapper<'s, S: pdb::Source<'s> + 's>(pdb: &mut pdb::PDB<'s, S>) -> std::result::Result<(), srcsrv::ParseError> { /// if let Ok(srcsrv_stream) = pdb.named_stream(b"srcsrv") { /// let stream = SrcSrvStream::parse(srcsrv_stream.as_slice())?; /// } /// # Ok(()) /// # } /// ``` pub fn parse(stream: &'a [u8]) -> Result, ParseError> { let stream = std::str::from_utf8(stream).map_err(|_| ParseError::InvalidUtf8)?; let mut lines = stream.lines(); // Parse section SRCSRV: ini ------------------------------------------------ let first_line = lines.next().ok_or(ParseError::UnexpectedEof)?; if !first_line.starts_with("SRCSRV: ini --") { return Err(ParseError::MissingIniSection); } let mut ini_fields = HashMap::new(); let next_section_start_line = loop { let line = lines.next().ok_or(ParseError::UnexpectedEof)?; if line.starts_with("SRCSRV:") { break line; } let (name, value) = line.split_once('=').ok_or(ParseError::MissingEquals)?; ini_fields.insert(name.to_ascii_lowercase(), value); }; let version = match ini_fields.get(&"VERSION".to_ascii_lowercase()) { Some(&"1") => 1, Some(&"2") => 2, Some(&"3") => 3, Some(v) => return Err(ParseError::UnrecognizedVersion(v.to_string())), None => return Err(ParseError::MissingVersion), }; // Parse section SRCSRV: variables ------------------------------------------ if !next_section_start_line.starts_with("SRCSRV: variables --") { return Err(ParseError::MissingVariablesSection); } let mut var_fields = HashMap::new(); let next_section_start_line = loop { let line = lines.next().ok_or(ParseError::UnexpectedEof)?; if line.starts_with("SRCSRV:") { break line; } let (name, value) = line.split_once('=').ok_or(ParseError::MissingEquals)?; let node = AstNode::parse(value)?; var_fields.insert(name.to_ascii_lowercase(), (value, node)); }; if !var_fields.contains_key(&"SRCSRVTRG".to_ascii_lowercase()) { return Err(ParseError::MissingSrcSrvTrgField); } // Parse section SRCSRV: source files --------------------------------------- if !next_section_start_line.starts_with("SRCSRV: source files --") { return Err(ParseError::MissingSourceFilesSection); } let mut source_file_entries = HashMap::new(); let end_line = loop { let line = lines.next().ok_or(ParseError::UnexpectedEof)?; if line.starts_with("SRCSRV:") { break line; } let vars: Vec<&str> = line.splitn(10, '*').collect(); source_file_entries.insert(vars[0].to_ascii_lowercase(), vars); }; // Stop at SRCSRV: end ------------------------------------------------ if !end_line.starts_with("SRCSRV: end --") { return Err(ParseError::MissingTerminationLine); } Ok(SrcSrvStream { version, ini_fields, var_fields, source_file_entries, }) } /// The value of the VERSION field from the ini section. pub fn version(&self) -> u8 { self.version } /// The value of the INDEXVERSION field from the ini section, if specified. pub fn index_version(&self) -> Option<&'a str> { self.ini_fields.get("indexversion").cloned() } /// The value of the DATETIME field from the ini section, if specified. pub fn datetime(&self) -> Option<&'a str> { self.ini_fields.get("datetime").cloned() } /// The value of the VERCTRL field from the ini section, if specified. pub fn version_control_description(&self) -> Option<&'a str> { self.ini_fields.get("verctrl").cloned() } /// Look up `original_file_path` in the file entries and find out how to obtain /// the source for this file. This evaluates the variables for the matching file /// entry. /// /// `extraction_base_path` is used as the value of the special `%targ%` variable /// and should not include a trailing backslash. /// /// Returns `Ok(None)` if the file path was not found in the list of file entries. /// /// ``` /// use srcsrv::{SrcSrvStream, SourceRetrievalMethod}; /// /// # fn wrapper() -> std::result::Result<(), Box> { /// # let stream = SrcSrvStream::parse(&[])?; /// println!( /// "{:#?}", /// stream.source_for_path( /// r#"C:\build\renderdoc\renderdoc\data\glsl\gl_texsample.h"#, /// r#"C:\Debugger\Cached Sources"# /// )? /// ); /// # Ok(()) /// # } /// ``` pub fn source_for_path( &self, original_file_path: &str, extraction_base_path: &str, ) -> Result, EvalError> { match self.source_and_raw_var_values_for_path(original_file_path, extraction_base_path)? { Some((method, _)) => Ok(Some(method)), None => Ok(None), } } /// Look up `original_file_path` in the file entries and find out how to obtain /// the source for this file. This evaluates the variables for the matching file /// entry. /// /// `extraction_base_path` is used as the value of the special `%targ%` variable /// and should not include a trailing backslash. /// /// This method additionally returns the raw values of all variables. This gives /// consumers more ways to special-case their behavior. It also acts as an escape /// hatch if there are any cases that `SourceRetrievalMethod` does not cover. /// If you don't need the raw variable values, prefer to call `source_for_path` /// instead. /// /// Returns `Ok(None)` if the file path was not found in the list of file entries. pub fn source_and_raw_var_values_for_path( &self, original_file_path: &str, extraction_base_path: &str, ) -> Result, EvalError> { let mut map = match self.vars_for_file(original_file_path)? { Some(map) => map, None => return Ok(None), }; let error_persistence_version_control = self .get_raw_var("SRCSRVERRVAR") .and_then(|var| map.get(&var.to_ascii_lowercase()).cloned()); map.insert("targ".to_string(), extraction_base_path.to_string()); let target = self.evaluate_required_field("SRCSRVTRG", &mut map)?; let command = self.evaluate_optional_field("SRCSRVCMD", &mut map)?; let env = self.evaluate_optional_field("SRCSRVENV", &mut map)?; let version_ctrl = self.evaluate_optional_field("SRCSRVVERCTRL", &mut map)?; if let Some(command) = command { let env = match env { Some(env) => env .split('\x08') .filter_map(|s| s.split_once('=')) .map(|(envname, envval)| (envname.to_owned(), envval.to_owned())) .collect(), None => HashMap::new(), }; return Ok(Some(( SourceRetrievalMethod::ExecuteCommand { command, env, target_path: target, version_ctrl, error_persistence_version_control, }, map, ))); } if target.starts_with("http://") || target.starts_with("https://") { return Ok(Some((SourceRetrievalMethod::Download { url: target }, map))); } Ok(Some(( SourceRetrievalMethod::Other { raw_var_values: map.clone(), }, map, ))) } /// A set of strings which can be substring-matched to the output of the /// command that executed when obtaining source files. /// /// If any of the strings matches, it is recommended to "persist the error" /// and refuse to execute further commands for other files with the same /// `error_persistence_version_control` value. pub fn error_persistence_command_output_strings(&self) -> HashSet<&'a str> { self.var_fields .iter() .filter_map(|(var_name, (var_value, _))| { if var_name.starts_with(&"SRCSRVERRDESC".to_ascii_lowercase()) { Some(*var_value) } else { None } }) .collect() } /// Get the value of the specified field from the ini section. /// The field name is case-insensitive. pub fn get_ini_field(&self, field_name: &str) -> Option<&'a str> { self.ini_fields .get(&field_name.to_ascii_lowercase()) .cloned() } /// Get the raw, unevaluated value of the specified field from the /// variables section. /// The field name is case-insensitive. pub fn get_raw_var(&self, var_name: &str) -> Option<&'a str> { self.var_fields .get(&var_name.to_ascii_lowercase()) .map(|(val, _)| *val) } /// Add the values of var1, ..., var10 to the map, for the given file path. /// Returns Ok(None) if the file was not found. fn vars_for_file(&self, file_path: &str) -> Result, EvalError> { let vars = match self .source_file_entries .get(&file_path.to_ascii_lowercase()) { Some(vars) => vars, None => return Ok(None), }; Ok(Some( vars.iter() .enumerate() .map(|(i, var)| (format!("var{}", i + 1), var.to_string())) .collect(), )) } fn evaluate_optional_field( &self, var_name: &str, var_map: &mut EvalVarMap, ) -> Result, EvalError> { let var_name = var_name.to_ascii_lowercase(); if !self.var_fields.contains_key(&var_name) { return Ok(None); } let val = self.eval_impl(var_name, var_map, &mut vec![])?; Ok(Some(val)) } fn evaluate_required_field( &self, var_name: &str, var_map: &mut EvalVarMap, ) -> Result { let var_name = var_name.to_ascii_lowercase(); self.eval_impl(var_name, var_map, &mut vec![]) } fn eval_impl( &self, var_name: String, var_map: &mut EvalVarMap, eval_stack: &mut Vec, ) -> Result { if let Some(val) = var_map.get(&var_name) { return Ok(val.clone()); } if eval_stack.contains(&var_name) { return Err(EvalError::Recursion(var_name)); } eval_stack.push(var_name.clone()); let node = match self.var_fields.get(&var_name) { Some((_, node)) => node, None => return Err(EvalError::UnknownVariable(var_name)), }; let mut get_var = |var_name: &str| self.eval_impl(var_name.to_ascii_lowercase(), var_map, eval_stack); let eval_val = node.eval(&mut get_var)?; var_map.insert(var_name, eval_val.clone()); eval_stack.pop(); Ok(eval_val) } } #[cfg(test)] mod tests { use std::collections::HashMap; use crate::{SourceRetrievalMethod, SrcSrvStream}; #[test] fn firefox() { let stream = r#"SRCSRV: ini ------------------------------------------------ VERSION=2 INDEXVERSION=2 VERCTRL=http SRCSRV: variables ------------------------------------------ HGSERVER=https://hg.mozilla.org/mozilla-central SRCSRVVERCTRL=http HTTP_EXTRACT_TARGET=%hgserver%/raw-file/%var3%/%var2% SRCSRVTRG=%http_extract_target% SRCSRV: source files --------------------------------------- /builds/worker/checkouts/gecko/mozglue/build/SSE.cpp*mozglue/build/SSE.cpp*1706d4d54ec68fae1280305b70a02cb24c16ff68 /builds/worker/checkouts/gecko/memory/build/mozjemalloc.cpp*memory/build/mozjemalloc.cpp*1706d4d54ec68fae1280305b70a02cb24c16ff68 /builds/worker/checkouts/gecko/vs2017_15.8.4/VC/include/algorithm*vs2017_15.8.4/VC/include/algorithm*1706d4d54ec68fae1280305b70a02cb24c16ff68 /builds/worker/checkouts/gecko/mozglue/baseprofiler/core/ProfilerBacktrace.cpp*mozglue/baseprofiler/core/ProfilerBacktrace.cpp*1706d4d54ec68fae1280305b70a02cb24c16ff68 /builds/worker/workspace/obj-build/dist/include/mozilla/IntegerRange.h*mfbt/IntegerRange.h*1706d4d54ec68fae1280305b70a02cb24c16ff68 SRCSRV: end ------------------------------------------------ "#; let stream = SrcSrvStream::parse(stream.as_bytes()).unwrap(); assert_eq!(stream.version(), 2); assert_eq!(stream.datetime(), None); assert_eq!(stream.version_control_description(), Some("http")); assert_eq!( stream .source_for_path( r#"/builds/worker/checkouts/gecko/mozglue/baseprofiler/core/ProfilerBacktrace.cpp"#, r#"C:\Debugger\Cached Sources"# ) .unwrap().unwrap(), SourceRetrievalMethod::Download { url: "https://hg.mozilla.org/mozilla-central/raw-file/1706d4d54ec68fae1280305b70a02cb24c16ff68/mozglue/baseprofiler/core/ProfilerBacktrace.cpp".to_string() } ); } #[test] fn chrome() { // From https://chromium-browser-symsrv.commondatastorage.googleapis.com/chrome.dll.pdb/5D664C4A228FA9804C4C44205044422E1/chrome.dll.pdb let stream = r#"SRCSRV: ini ------------------------------------------------ VERSION=1 INDEXVERSION=2 VERCTRL=Subversion DATETIME=Fri Jul 30 14:11:46 2021 SRCSRV: variables ------------------------------------------ SRC_EXTRACT_TARGET_DIR=%targ%\%fnbksl%(%var2%)\%var3% SRC_EXTRACT_TARGET=%SRC_EXTRACT_TARGET_DIR%\%fnfile%(%var1%) SRC_EXTRACT_CMD=cmd /c "mkdir "%SRC_EXTRACT_TARGET_DIR%" & python -c "import urllib2, base64;url = \"%var4%\";u = urllib2.urlopen(url);open(r\"%SRC_EXTRACT_TARGET%\", \"wb\").write(%var5%(u.read()))" SRCSRVTRG=%SRC_EXTRACT_TARGET% SRCSRVCMD=%SRC_EXTRACT_CMD% SRCSRV: source files --------------------------------------- c:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fdrm\fx_crypt.cpp*core/fdrm/fx_crypt.cpp*dab1161c861cc239e48a17e1a5d729aa12785a53*https://pdfium.googlesource.com/pdfium.git/+/dab1161c861cc239e48a17e1a5d729aa12785a53/core/fdrm/fx_crypt.cpp?format=TEXT*base64.b64decode c:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fdrm\fx_crypt_aes.cpp*core/fdrm/fx_crypt_aes.cpp*dab1161c861cc239e48a17e1a5d729aa12785a53*https://pdfium.googlesource.com/pdfium.git/+/dab1161c861cc239e48a17e1a5d729aa12785a53/core/fdrm/fx_crypt_aes.cpp?format=TEXT*base64.b64decode SRCSRV: end ------------------------------------------------"#; let stream = SrcSrvStream::parse(stream.as_bytes()).unwrap(); assert_eq!(stream.version(), 1); assert_eq!(stream.datetime(), Some("Fri Jul 30 14:11:46 2021")); assert_eq!(stream.version_control_description(), Some("Subversion")); assert_eq!( stream .source_for_path( r#"c:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fdrm\fx_crypt.cpp"#, r#"C:\Debugger\Cached Sources"#, ) .unwrap().unwrap(), SourceRetrievalMethod::ExecuteCommand { command: r#"cmd /c "mkdir "C:\Debugger\Cached Sources\core\fdrm\fx_crypt.cpp\dab1161c861cc239e48a17e1a5d729aa12785a53" & python -c "import urllib2, base64;url = \"https://pdfium.googlesource.com/pdfium.git/+/dab1161c861cc239e48a17e1a5d729aa12785a53/core/fdrm/fx_crypt.cpp?format=TEXT\";u = urllib2.urlopen(url);open(r\"C:\Debugger\Cached Sources\core\fdrm\fx_crypt.cpp\dab1161c861cc239e48a17e1a5d729aa12785a53\fx_crypt.cpp\", \"wb\").write(base64.b64decode(u.read()))""#.to_string(), env: HashMap::new(), target_path: r#"C:\Debugger\Cached Sources\core\fdrm\fx_crypt.cpp\dab1161c861cc239e48a17e1a5d729aa12785a53\fx_crypt.cpp"#.to_string(), version_ctrl: None, error_persistence_version_control: None, } ); } #[test] fn team_foundation() { // From https://github.com/microsoft/perfview/blob/5c9f6059f54db41b4ac5c4fc8f57261779634489/src/TraceEvent/Symbols/NativeSymbolModule.cs#L776 let stream = r#"SRCSRV: ini ------------------------------------------------ VERSION=3 INDEXVERSION=2 VERCTRL=Team Foundation Server DATETIME=Thu Mar 10 16:15:55 2016 SRCSRV: variables ------------------------------------------ TFS_EXTRACT_CMD=tf.exe view /version:%var4% /noprompt "$%var3%" /server:%fnvar%(%var2%) /output:%srcsrvtrg% TFS_EXTRACT_TARGET=%targ%\%var2%%fnbksl%(%var3%)\%var4%\%fnfile%(%var1%) VSTFDEVDIV_DEVDIV2=http://vstfdevdiv.redmond.corp.microsoft.com:8080/DevDiv2 SRCSRVVERCTRL=tfs SRCSRVERRDESC=access SRCSRVERRVAR=var2 SRCSRVTRG=%TFS_extract_target% SRCSRVCMD=%TFS_extract_cmd% SRCSRV: source files --------------------------------------- f:\dd\externalapis\legacy\vctools\vc12\inc\cvconst.h*VSTFDEVDIV_DEVDIV2*/DevDiv/Fx/Rel/NetFxRel3Stage/externalapis/legacy/vctools/vc12/inc/cvconst.h*1363200 f:\dd\externalapis\legacy\vctools\vc12\inc\cvinfo.h*VSTFDEVDIV_DEVDIV2*/DevDiv/Fx/Rel/NetFxRel3Stage/externalapis/legacy/vctools/vc12/inc/cvinfo.h*1363200 f:\dd\externalapis\legacy\vctools\vc12\inc\vc\ammintrin.h*VSTFDEVDIV_DEVDIV2*/DevDiv/Fx/Rel/NetFxRel3Stage/externalapis/legacy/vctools/vc12/inc/vc/ammintrin.h*1363200 SRCSRV: end ------------------------------------------------"#; let stream = SrcSrvStream::parse(stream.as_bytes()).unwrap(); assert_eq!(stream.version(), 3); assert_eq!(stream.datetime(), Some("Thu Mar 10 16:15:55 2016")); assert_eq!( stream.version_control_description(), Some("Team Foundation Server") ); assert_eq!( stream .source_for_path( r#"F:\dd\externalapis\legacy\vctools\vc12\inc\cvinfo.h"#, r#"C:\Debugger\Cached Sources"#, ) .unwrap().unwrap(), SourceRetrievalMethod::ExecuteCommand { command: r#"tf.exe view /version:1363200 /noprompt "$/DevDiv/Fx/Rel/NetFxRel3Stage/externalapis/legacy/vctools/vc12/inc/cvinfo.h" /server:http://vstfdevdiv.redmond.corp.microsoft.com:8080/DevDiv2 /output:C:\Debugger\Cached Sources\VSTFDEVDIV_DEVDIV2\DevDiv\Fx\Rel\NetFxRel3Stage\externalapis\legacy\vctools\vc12\inc\cvinfo.h\1363200\cvinfo.h"#.to_string(), env: HashMap::new(), version_ctrl: Some("tfs".to_string()), target_path: r#"C:\Debugger\Cached Sources\VSTFDEVDIV_DEVDIV2\DevDiv\Fx\Rel\NetFxRel3Stage\externalapis\legacy\vctools\vc12\inc\cvinfo.h\1363200\cvinfo.h"#.to_string(), error_persistence_version_control: Some("VSTFDEVDIV_DEVDIV2".to_string()), } ); } #[test] fn renderdoc() { // From https://renderdoc.org/symbols/renderdoc.pdb/6D1DFFC4DC524537962CCABC000820641/renderdoc.pd_ let stream = r#"SRCSRV: ini ------------------------------------------------ VERSION=2 VERCTRL=http SRCSRV: variables ------------------------------------------ HTTP_ALIAS=https://raw.githubusercontent.com/baldurk/renderdoc/v1.15/ HTTP_EXTRACT_TARGET=%HTTP_ALIAS%%var2% SRCSRVTRG=%HTTP_EXTRACT_TARGET% SRCSRV: source files --------------------------------------- C:\build\renderdoc\qrenderdoc\Code\BufferFormatter.cpp*qrenderdoc/Code/BufferFormatter.cpp C:\build\renderdoc\qrenderdoc\Windows\Dialogs\AnalyticsConfirmDialog.cpp*qrenderdoc/Windows/Dialogs/AnalyticsConfirmDialog.cpp C:\build\renderdoc\renderdoc\data\glsl\gl_texsample.h*renderdoc/data/glsl/gl_texsample.h C:\build\renderdoc\renderdoc\driver\d3d12\d3d12_device.cpp*renderdoc/driver/d3d12/d3d12_device.cpp C:\build\renderdoc\renderdoc\maths\matrix.cpp*renderdoc/maths/matrix.cpp C:\build\renderdoc\util\test\demos\texture_zoo.cpp*util/test/demos/texture_zoo.cpp C:\build\renderdoc\Win32\Release\renderdoc_app.h*Win32/Release/renderdoc_app.h C:\build\renderdoc\x64\Release\renderdoc_app.h*x64/Release/renderdoc_app.h SRCSRV: end ------------------------------------------------"#; let stream = SrcSrvStream::parse(stream.as_bytes()).unwrap(); assert_eq!(stream.version(), 2); assert_eq!(stream.datetime(), None); assert_eq!(stream.version_control_description(), Some("http")); assert_eq!( stream .source_for_path( r#"C:\build\renderdoc\renderdoc\data\glsl\gl_texsample.h"#, r#"C:\Debugger\Cached Sources"#, ) .unwrap().unwrap(), SourceRetrievalMethod::Download { url: "https://raw.githubusercontent.com/baldurk/renderdoc/v1.15/renderdoc/data/glsl/gl_texsample.h".to_string(), } ); } }