Skip to content
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 src/api/data_types/chunking/upload/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub enum ChunkUploadCapability {
/// Upload of il2cpp line mappings
Il2Cpp,

/// Upload of Dart symbol maps
DartSymbolMap,

/// Upload of preprod artifacts
PreprodArtifacts,

Expand All @@ -52,6 +55,7 @@ impl<'de> Deserialize<'de> for ChunkUploadCapability {
"sources" => ChunkUploadCapability::Sources,
"bcsymbolmaps" => ChunkUploadCapability::BcSymbolmap,
"il2cpp" => ChunkUploadCapability::Il2Cpp,
"dartsymbolmap" => ChunkUploadCapability::DartSymbolMap,
"preprod_artifacts" => ChunkUploadCapability::PreprodArtifacts,
_ => ChunkUploadCapability::Unknown,
})
Expand Down
46 changes: 46 additions & 0 deletions src/commands/dart_symbol_map/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use anyhow::Result;
use clap::{ArgMatches, Args, Command, Parser as _, Subcommand};

pub mod upload;

const GROUP_ABOUT: &str = "Manage Dart/Flutter symbol maps for Sentry.";
const UPLOAD_ABOUT: &str =
"Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.";
const UPLOAD_LONG_ABOUT: &str =
"Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.{n}{n}Examples:{n} sentry-cli dart-symbol-map upload --org my-org --project my-proj path/to/dartsymbolmap.json path/to/debug/file{n}{n}The mapping must be a JSON array of strings with an even number of entries (pairs).{n}The debug file must contain exactly one Debug ID.";

#[derive(Args)]
pub(super) struct DartSymbolMapArgs {
#[command(subcommand)]
pub(super) subcommand: DartSymbolMapSubcommand,
}

#[derive(Subcommand)]
#[command(about = GROUP_ABOUT)]
pub(super) enum DartSymbolMapSubcommand {
#[command(about = UPLOAD_ABOUT)]
#[command(long_about = UPLOAD_LONG_ABOUT)]
Upload(upload::DartSymbolMapUploadArgs),
}

pub(super) fn make_command(command: Command) -> Command {
DartSymbolMapSubcommand::augment_subcommands(
command
.about(GROUP_ABOUT)
.subcommand_required(true)
.arg_required_else_help(true),
)
}

pub(super) fn execute(_: &ArgMatches) -> Result<()> {
let subcommand = match crate::commands::derive_parser::SentryCLI::parse().command {
crate::commands::derive_parser::SentryCLICommand::DartSymbolMap(DartSymbolMapArgs {
subcommand,
}) => subcommand,
_ => unreachable!("expected dart-symbol-map subcommand"),
};

match subcommand {
DartSymbolMapSubcommand::Upload(args) => upload::execute(args),
}
}
180 changes: 180 additions & 0 deletions src/commands/dart_symbol_map/upload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::path::Path;

use anyhow::{bail, Context as _, Result};
use clap::Args;

use crate::api::{Api, ChunkUploadCapability};
use crate::config::Config;
use crate::constants::{DEFAULT_MAX_DIF_SIZE, DEFAULT_MAX_WAIT};
use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked};
use crate::utils::dif::DifFile;
use symbolic::common::ByteView;
use symbolic::common::DebugId;

struct DartSymbolMapObject<'a> {
bytes: &'a [u8],
name: &'a str,
debug_id: DebugId,
}

impl<'a> AsRef<[u8]> for DartSymbolMapObject<'a> {
fn as_ref(&self) -> &[u8] {
self.bytes
}
}

impl<'a> Display for DartSymbolMapObject<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "dartsymbolmap {}", self.name)
}
}

impl<'a> Assemblable for DartSymbolMapObject<'a> {
fn name(&self) -> Cow<'_, str> {
Cow::Borrowed(self.name)
}

fn debug_id(&self) -> Option<DebugId> {
Some(self.debug_id)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: DartSymbolMap Uploads Misclassified

The DartSymbolMapObject's Assemblable implementation is missing the object type. This causes chunked upload assembly requests to default to "debug_files" instead of "dartsymbolmap". Consequently, uploads may be rejected by servers that only support dartsymbolmap types (despite the capability check passing), or the uploaded files may be misclassified as generic debug files, preventing their use for Dart deobfuscation.

Fix in Cursor Fix in Web


#[derive(Args, Clone)]
pub(crate) struct DartSymbolMapUploadArgs {
#[arg(short = 'o', long = "org")]
#[arg(help = "The organization ID or slug.")]
pub(super) org: Option<String>,

#[arg(short = 'p', long = "project")]
#[arg(help = "The project ID or slug.")]
pub(super) project: Option<String>,

#[arg(value_name = "MAPPING")]
#[arg(
help = "Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs)."
)]
pub(super) mapping: String,

#[arg(value_name = "DEBUG_FILE")]
#[arg(
help = "Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID."
)]
pub(super) debug_file: String,
}

pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
let mapping_path = &args.mapping;
let debug_file_path = &args.debug_file;

// Extract Debug ID(s) from the provided debug file
let dif = DifFile::open_path(debug_file_path, None)?;
let mut ids: Vec<DebugId> = dif.ids().filter(|id| !id.is_nil()).collect();

// Ensure a single, unambiguous Debug ID
ids.sort();
ids.dedup();
match ids.len() {
0 => bail!(
"No debug identifier found in the provided debug file ({}). Ensure the file contains an embedded Debug ID.",
debug_file_path
),
1 => {
let debug_id = ids.remove(0);

// Validate the dartsymbolmap JSON: must be a JSON array of strings with even length
let mapping_file_bytes = ByteView::open(mapping_path)
.with_context(|| format!("Failed to read mapping file at {mapping_path}"))?;
let mapping_entries: Vec<Cow<'_, str>> =
serde_json::from_slice(mapping_file_bytes.as_ref())
.context("Invalid dartsymbolmap: expected a JSON array of strings")?;

if mapping_entries.len() % 2 != 0 {
bail!(
"Invalid dartsymbolmap: expected an even number of entries, got {}",
mapping_entries.len()
);
}

// Prepare upload object
let file_name = Path::new(mapping_path)
.file_name()
.and_then(OsStr::to_str)
.unwrap_or(mapping_path)
;

let mapping_len = mapping_file_bytes.len();
let object = DartSymbolMapObject {
bytes: mapping_file_bytes.as_ref(),
name: file_name,
debug_id,
};

// Prepare chunked upload
let api = Api::current();
// Resolve org and project like logs: prefer args, fallback to defaults
let config = Config::current();
let (default_org, default_project) = config.get_org_and_project_defaults();
let org = args
.org
.as_ref()
.or(default_org.as_ref())
.ok_or_else(|| anyhow::anyhow!(
"No organization specified. Please specify an organization using the --org argument."
))?;
let project = args
.project
.as_ref()
.or(default_project.as_ref())
.ok_or_else(|| anyhow::anyhow!(
"No project specified. Use --project or set a default in config."
))?;
let chunk_upload_options = api
.authenticated()?
.get_chunk_upload_options(org)?
.ok_or_else(|| anyhow::anyhow!(
"server does not support chunked uploading. Please update your Sentry server."
))?;

if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) {
bail!(
"Server does not support uploading Dart symbol maps via chunked upload. Please update your Sentry server."
);
}

// Early file size check against server or default limits (same as debug files)
let effective_max_file_size = if chunk_upload_options.max_file_size > 0 {
chunk_upload_options.max_file_size
} else {
DEFAULT_MAX_DIF_SIZE
};

if (mapping_len as u64) > effective_max_file_size {
bail!(
"The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).",
mapping_path,
mapping_len,
effective_max_file_size
);
}

let options = ChunkOptions::new(chunk_upload_options, org, project)
.with_max_wait(DEFAULT_MAX_WAIT);

let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?;
let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?;
if has_processing_errors {
bail!("Some symbol maps did not process correctly");
}

Ok(())
}
_ => bail!(
"Multiple debug identifiers found in the provided debug file ({}): {}. Please provide a file that contains a single Debug ID.",
debug_file_path,
ids.into_iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", ")
),
}
}
2 changes: 2 additions & 0 deletions src/commands/derive_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::utils::auth_token::AuthToken;
use crate::utils::value_parsers::{auth_token_parser, kv_parser};
use clap::{command, ArgAction::SetTrue, Parser, Subcommand};

use super::dart_symbol_map::DartSymbolMapArgs;
use super::logs::LogsArgs;
use super::send_metric::SendMetricArgs;

Expand Down Expand Up @@ -35,4 +36,5 @@ pub(super) struct SentryCLI {
pub(super) enum SentryCLICommand {
Logs(LogsArgs),
SendMetric(SendMetricArgs),
DartSymbolMap(DartSymbolMapArgs),
}
2 changes: 2 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::utils::update::run_sentrycli_update_nagger;
use crate::utils::value_parsers::auth_token_parser;

mod bash_hook;
mod dart_symbol_map;
mod debug_files;
mod deploys;
mod derive_parser;
Expand Down Expand Up @@ -71,6 +72,7 @@ macro_rules! each_subcommand {
$mac!(send_envelope);
$mac!(send_metric);
$mac!(sourcemaps);
$mac!(dart_symbol_map);
#[cfg(not(feature = "managed"))]
$mac!(uninstall);
#[cfg(not(feature = "managed"))]
Expand Down
1 change: 1 addition & 0 deletions tests/integration/_cases/help/help-windows.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Commands:
send-event Send a manual event to Sentry.
send-envelope Send a stored envelope to Sentry.
sourcemaps Manage sourcemaps for Sentry releases.
dart-symbol-map Manage Dart/Flutter symbol maps for Sentry.
upload-proguard Upload ProGuard mapping files to a project.
help Print this message or the help of the given subcommand(s)

Expand Down
1 change: 1 addition & 0 deletions tests/integration/_cases/help/help.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Commands:
send-event Send a manual event to Sentry.
send-envelope Send a stored envelope to Sentry.
sourcemaps Manage sourcemaps for Sentry releases.
dart-symbol-map Manage Dart/Flutter symbol maps for Sentry.
uninstall Uninstall the sentry-cli executable.
upload-proguard Upload ProGuard mapping files to a project.
help Print this message or the help of the given subcommand(s)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"MaterialApp",
"ex",
"Scaffold"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
"MaterialApp",
"ex",
"Scaffold",
"ey"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"url": "organizations/wat-org/chunk-upload/",
"chunkSize": 8388608,
"chunksPerRequest": 64,
"maxFileSize": 2147483648,
"maxRequestSize": 33554432,
"concurrency": 8,
"hashAlgorithm": "sha1",
"compression": ["gzip"],
"accept": ["dartsymbolmap"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"url": "organizations/wat-org/chunk-upload/",
"chunkSize": 8388608,
"chunksPerRequest": 64,
"maxFileSize": 2147483648,
"maxRequestSize": 33554432,
"concurrency": 8,
"hashAlgorithm": "sha1",
"compression": ["gzip"],
"accept": ["debug_files", "dartsymbolmap"]
}
1 change: 1 addition & 0 deletions tests/integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod test_utils;
mod token_validation;
mod uninstall;
mod update;
mod upload_dart_symbol_map;
mod upload_dif;
mod upload_dsym;
mod upload_proguard;
Expand Down
Loading