|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +use dsc_lib::configure::config_doc::Configuration; |
| 5 | +use dsc_lib::util::parse_input_to_json; |
| 6 | +use schemars::JsonSchema; |
| 7 | +use serde::{Deserialize, Serialize}; |
| 8 | +use std::io::Read; |
| 9 | +use std::fs::File; |
| 10 | +use std::path::{Path, PathBuf}; |
| 11 | +use tracing::{debug, info}; |
| 12 | + |
| 13 | +use crate::util::DSC_CONFIG_ROOT; |
| 14 | + |
| 15 | +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] |
| 16 | +pub struct Include { |
| 17 | + /// The path to the file to include. Path is relative to the file containing the include |
| 18 | + /// and not allowed to reference parent directories. If a configuration document is used |
| 19 | + /// instead of a file, then the path is relative to the current working directory. |
| 20 | + #[serde(rename = "configurationFile")] |
| 21 | + pub configuration_file: String, |
| 22 | + #[serde(rename = "parametersFile")] |
| 23 | + pub parameters_file: Option<String>, |
| 24 | +} |
| 25 | + |
| 26 | +/// Read the file specified in the Include input and return the content as a JSON string. |
| 27 | +/// |
| 28 | +/// # Arguments |
| 29 | +/// |
| 30 | +/// * `input` - The Include input as a JSON string. |
| 31 | +/// |
| 32 | +/// # Returns |
| 33 | +/// |
| 34 | +/// A tuple containing the contents of the parameters file as JSON and the configuration content |
| 35 | +/// as a JSON string. |
| 36 | +/// |
| 37 | +/// # Errors |
| 38 | +/// |
| 39 | +/// This function will return an error if the Include input is not valid JSON, if the file |
| 40 | +/// specified in the Include input cannot be read, or if the content of the file cannot be |
| 41 | +/// deserialized as YAML or JSON. |
| 42 | +pub fn get_contents(input: &str) -> Result<(Option<String>, String), String> { |
| 43 | + debug!("Processing Include input"); |
| 44 | + |
| 45 | + // deserialize the Include input |
| 46 | + let include = match serde_json::from_str::<Include>(input) { |
| 47 | + Ok(include) => include, |
| 48 | + Err(err) => { |
| 49 | + return Err(format!("Error: Failed to deserialize Include input: {err}")); |
| 50 | + } |
| 51 | + }; |
| 52 | + |
| 53 | + let include_path = normalize_path(Path::new(&include.configuration_file))?; |
| 54 | + |
| 55 | + // read the file specified in the Include input |
| 56 | + let mut buffer: Vec<u8> = Vec::new(); |
| 57 | + match File::open(&include_path) { |
| 58 | + Ok(mut file) => { |
| 59 | + match file.read_to_end(&mut buffer) { |
| 60 | + Ok(_) => (), |
| 61 | + Err(err) => { |
| 62 | + return Err(format!("Error: Failed to read file '{include_path:?}': {err}")); |
| 63 | + } |
| 64 | + } |
| 65 | + }, |
| 66 | + Err(err) => { |
| 67 | + return Err(format!("Error: Failed to open included file '{include_path:?}': {err}")); |
| 68 | + } |
| 69 | + } |
| 70 | + // convert the buffer to a string |
| 71 | + let include_content = match String::from_utf8(buffer) { |
| 72 | + Ok(input) => input, |
| 73 | + Err(err) => { |
| 74 | + return Err(format!("Error: Invalid UTF-8 sequence in included file '{include_path:?}': {err}")); |
| 75 | + } |
| 76 | + }; |
| 77 | + |
| 78 | + // try to deserialize the Include content as YAML first |
| 79 | + let configuration: Configuration = match serde_yaml::from_str(&include_content) { |
| 80 | + Ok(configuration) => configuration, |
| 81 | + Err(_err) => { |
| 82 | + // if that fails, try to deserialize it as JSON |
| 83 | + match serde_json::from_str(&include_content) { |
| 84 | + Ok(configuration) => configuration, |
| 85 | + Err(err) => { |
| 86 | + return Err(format!("Error: Failed to read the configuration file '{include_path:?}' as YAML or JSON: {err}")); |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + }; |
| 91 | + |
| 92 | + // serialize the Configuration as JSON |
| 93 | + let config_json = match serde_json::to_string(&configuration) { |
| 94 | + Ok(json) => json, |
| 95 | + Err(err) => { |
| 96 | + return Err(format!("Error: JSON Error: {err}")); |
| 97 | + } |
| 98 | + }; |
| 99 | + |
| 100 | + let parameters = if let Some(parameters_file) = include.parameters_file { |
| 101 | + // combine the path with DSC_CONFIG_ROOT |
| 102 | + let parameters_file = normalize_path(Path::new(¶meters_file))?; |
| 103 | + info!("Resolving parameters from file '{parameters_file:?}'"); |
| 104 | + match std::fs::read_to_string(¶meters_file) { |
| 105 | + Ok(parameters) => { |
| 106 | + let parameters_json = match parse_input_to_json(¶meters) { |
| 107 | + Ok(json) => json, |
| 108 | + Err(err) => { |
| 109 | + return Err(format!("Failed to parse parameters file '{parameters_file:?}' to JSON: {err}")); |
| 110 | + } |
| 111 | + }; |
| 112 | + Some(parameters_json) |
| 113 | + }, |
| 114 | + Err(err) => { |
| 115 | + return Err(format!("Failed to resolve parameters file '{parameters_file:?}': {err}")); |
| 116 | + } |
| 117 | + } |
| 118 | + } else { |
| 119 | + debug!("No parameters file found"); |
| 120 | + None |
| 121 | + }; |
| 122 | + |
| 123 | + Ok((parameters, config_json)) |
| 124 | +} |
| 125 | + |
| 126 | +fn normalize_path(path: &Path) -> Result<PathBuf, String> { |
| 127 | + if path.is_absolute() { |
| 128 | + Ok(path.to_path_buf()) |
| 129 | + } else { |
| 130 | + // check that no components of the path are '..' |
| 131 | + if path.components().any(|c| c == std::path::Component::ParentDir) { |
| 132 | + return Err(format!("Error: Include path must not contain '..': {path:?}")); |
| 133 | + } |
| 134 | + |
| 135 | + // use DSC_CONFIG_ROOT env var as current directory |
| 136 | + let current_directory = match std::env::var(DSC_CONFIG_ROOT) { |
| 137 | + Ok(current_directory) => current_directory, |
| 138 | + Err(_err) => { |
| 139 | + // use current working directory |
| 140 | + match std::env::current_dir() { |
| 141 | + Ok(current_directory) => current_directory.to_string_lossy().into_owned(), |
| 142 | + Err(err) => { |
| 143 | + return Err(format!("Error: Failed to get current directory: {err}")); |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + }; |
| 148 | + |
| 149 | + // combine the current directory with the Include path |
| 150 | + Ok(Path::new(¤t_directory).join(path)) |
| 151 | + } |
| 152 | +} |
0 commit comments