Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter allowed process type characters #237

Merged
merged 2 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions buildpacks/dotnet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- The buildpack now sanitizes launch process type names, based on project assembly names, by filtering out invalid characters. ([#237](https://github.com/heroku/buildpacks-dotnet/pull/237))

## [0.3.5] - 2025-03-19

### Added
Expand Down
76 changes: 44 additions & 32 deletions buildpacks/dotnet/src/launch_process.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
use crate::dotnet::project::ProjectType;
use crate::dotnet::solution::Solution;
use crate::Project;
use libcnb::data::launch::{Process, ProcessBuilder, ProcessType, ProcessTypeError};
use libcnb::data::launch::{Process, ProcessBuilder, ProcessType};
use std::path::PathBuf;

#[derive(Debug)]
pub(crate) enum LaunchProcessDetectionError {
ProcessType(ProcessTypeError),
}

/// Detects processes in a solution's projects
pub(crate) fn detect_solution_processes(
solution: &Solution,
) -> Result<Vec<Process>, LaunchProcessDetectionError> {
pub(crate) fn detect_solution_processes(solution: &Solution) -> Vec<Process> {
solution
.projects
.iter()
.filter_map(|project| project_launch_process(solution, project))
.collect::<Result<_, _>>()
.collect()
}

/// Determines if a project should have a launchable process and constructs it
fn project_launch_process(
solution: &Solution,
project: &Project,
) -> Option<Result<Process, LaunchProcessDetectionError>> {
fn project_launch_process(solution: &Solution, project: &Project) -> Option<Process> {
if !matches!(
project.project_type,
ProjectType::ConsoleApplication | ProjectType::WebApplication | ProjectType::WorkerService
Expand All @@ -49,18 +39,14 @@ fn project_launch_process(
command.push_str(" --urls http://*:$PORT");
}

Some(
project_process_type(project).map(|process_type| {
ProcessBuilder::new(process_type, ["bash", "-c", &command]).build()
}),
)
Some(ProcessBuilder::new(project_process_type(project), ["bash", "-c", &command]).build())
}

fn project_process_type(project: &Project) -> Result<ProcessType, LaunchProcessDetectionError> {
project
.assembly_name
/// Returns a sanitized process type name, ensuring it is always valid
fn project_process_type(project: &Project) -> ProcessType {
sanitize_process_type_name(&project.assembly_name)
.parse::<ProcessType>()
.map_err(LaunchProcessDetectionError::ProcessType)
.expect("Sanitized process type name should always be valid")
}

/// Returns the (expected) relative executable path from the solution's parent directory
Expand All @@ -87,6 +73,14 @@ fn project_executable_path(project: &Project) -> PathBuf {
.join(&project.assembly_name)
}

/// Sanitizes a process type name to only contain allowed characters
fn sanitize_process_type_name(input: &str) -> String {
input
.chars()
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -118,10 +112,7 @@ mod tests {
working_directory: WorkingDirectory::App,
}];

assert_eq!(
detect_solution_processes(&solution).unwrap(),
expected_processes
);
assert_eq!(detect_solution_processes(&solution), expected_processes);
}

#[test]
Expand All @@ -148,10 +139,7 @@ mod tests {
working_directory: WorkingDirectory::App,
}];

assert_eq!(
detect_solution_processes(&solution).unwrap(),
expected_processes
);
assert_eq!(detect_solution_processes(&solution), expected_processes);
}

#[test]
Expand All @@ -166,7 +154,7 @@ mod tests {
}],
};

assert!(detect_solution_processes(&solution).unwrap().is_empty());
assert!(detect_solution_processes(&solution).is_empty());
}

#[test]
Expand Down Expand Up @@ -203,4 +191,28 @@ mod tests {
PathBuf::from("project/bin/publish/TestApp")
);
}

#[test]
fn test_sanitize_process_type_name() {
assert_eq!(
sanitize_process_type_name("Hello, world! 123"),
"Helloworld123"
);
assert_eq!(
sanitize_process_type_name("This_is-a.test.123.abc"),
"This_is-a.test.123.abc"
);
assert_eq!(
sanitize_process_type_name("Special chars: !@#$%+^&*()"),
"Specialchars"
);
assert_eq!(
sanitize_process_type_name("Mixed: aBc123.xyz_-!@#"),
"MixedaBc123.xyz_-"
);
assert_eq!(
sanitize_process_type_name("Unicode: 日本語123"),
"Unicode123"
);
}
}
79 changes: 19 additions & 60 deletions buildpacks/dotnet/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ use crate::dotnet_buildpack_configuration::{
DotnetBuildpackConfiguration, DotnetBuildpackConfigurationError, ExecutionEnvironment,
};
use crate::dotnet_publish_command::DotnetPublishCommand;
use crate::launch_process::LaunchProcessDetectionError;
use crate::layers::sdk::SdkLayerError;
use bullet_stream::global::print;
use bullet_stream::style;
use fun_run::CommandWithName;
use indoc::formatdoc;
use inventory::artifact::{Arch, Os};
use inventory::{Inventory, ParseInventoryError};
use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
Expand Down Expand Up @@ -182,34 +180,26 @@ impl Buildpack for DotnetBuildpack {

print::bullet("Process types");
print::sub_bullet("Detecting process types from published artifacts");
match launch_process::detect_solution_processes(&solution) {
Ok(processes) => {
if processes.is_empty() {
print::sub_bullet("No processes were detected");
} else {
for process in &processes {
print::sub_bullet(format!(
"Found {}: {}",
style::value(process.r#type.to_string()),
process.command.join(" ")
));
}
if Path::exists(&context.app_dir.join("Procfile")) {
print::sub_bullet("Procfile detected");
print::sub_bullet("Skipping process type registration (add process types to your Procfile as needed)");
} else {
launch_builder.processes(processes);
print::sub_bullet("No Procfile detected");
print::sub_bullet(
"Registering detected process types as launch processes",
);
};
}
}
Err(error) => {
log_launch_process_detection_warning(error);
let processes = launch_process::detect_solution_processes(&solution);
if processes.is_empty() {
print::sub_bullet("No processes were detected");
} else {
for process in &processes {
print::sub_bullet(format!(
"Found {}: {}",
style::value(process.r#type.to_string()),
process.command.join(" ")
));
}
};
if Path::exists(&context.app_dir.join("Procfile")) {
print::sub_bullet("Procfile detected");
print::sub_bullet("Skipping process type registration (add process types to your Procfile as needed)");
} else {
launch_builder.processes(processes);
print::sub_bullet("No Procfile detected");
print::sub_bullet("Registering detected process types as launch processes");
};
}
}
ExecutionEnvironment::Test => {
let mut args = vec![format!(
Expand Down Expand Up @@ -374,37 +364,6 @@ fn detect_global_json_sdk_configuration(
)
}

fn log_launch_process_detection_warning(error: LaunchProcessDetectionError) {
match error {
LaunchProcessDetectionError::ProcessType(process_type_error) => {
print::warning(formatdoc! {"
{process_type_error}

Launch process detection error

We detected an invalid launch process type.

The buildpack automatically tries to register Cloud Native Buildpacks (CNB)
process types for console and web projects after successfully publishing an
application.

Process type names are based on the filenames of compiled project executables,
which is usually the project name. For example, `webapi` for a `webapi.csproj`
project. In some cases, these names are be incompatible with the CNB spec as
process types can only contain numbers, letters, and the characters `.`, `_`,
and `-`.

To use this automatic launch process type registration, see the warning details
above to troubleshoot and make necessary adjustments.

If you think you found a bug in the buildpack, or have feedback on improving
the behavior for your use case, file an issue here:
https://github.com/heroku/buildpacks-dotnet/issues/new
"});
}
}
}

#[derive(Debug)]
enum DotnetBuildpackError {
BuildpackDetection(io::Error),
Expand Down