1use std::future::Future;
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use std::time::Duration;
7
8use crate::config::LogLevel;
9use crate::decrypt::redact_secrets;
10use crate::error::{Error, Result};
11use crate::event::ProgressEvent;
12use crate::manifest::{
13 Choice, EncryptionInfo, EncryptionMethod, MediaPart, MediaSegment, MediaType, MssData,
14 Playlist, RoleType, Stream,
15};
16
17#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct LogFilePlan {
20 pub enabled: bool,
22 pub path: Option<PathBuf>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct LogPlanConfig {
29 pub level: LogLevel,
31 pub no_log: bool,
33 pub log_file_path: Option<PathBuf>,
35 pub default_log_dir: PathBuf,
37 pub suffix: String,
39}
40
41#[derive(Clone, Debug, Default, Eq, PartialEq)]
43pub struct UpdateCheckResult {
44 pub latest_version: Option<String>,
46 pub update_available: bool,
48}
49
50pub trait UpdateCheckClient {
52 fn redirect_location<'a>(
54 &'a self,
55 url: &'a str,
56 ) -> Pin<Box<dyn Future<Output = Result<Option<String>>> + Send + 'a>>;
57}
58
59#[derive(Clone, Debug)]
61pub struct UpdateCheckHttpClient {
62 timeout: Duration,
63}
64
65pub const DEFAULT_UPDATE_CHECK_URL: &str = "https://github.com/bakgio/haki-dl/releases/latest";
67
68impl Default for UpdateCheckHttpClient {
69 fn default() -> Self {
70 Self {
71 timeout: Duration::from_secs(5),
72 }
73 }
74}
75
76impl UpdateCheckHttpClient {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_timeout(mut self, timeout: Duration) -> Self {
84 self.timeout = timeout;
85 self
86 }
87}
88
89impl UpdateCheckClient for UpdateCheckHttpClient {
90 fn redirect_location<'a>(
91 &'a self,
92 url: &'a str,
93 ) -> Pin<Box<dyn Future<Output = Result<Option<String>>> + Send + 'a>> {
94 Box::pin(async move {
95 let client = reqwest::Client::builder()
96 .timeout(self.timeout)
97 .redirect(reqwest::redirect::Policy::none())
98 .build()
99 .map_err(|error| Error::http(error.to_string()))?;
100 let response = client
101 .get(url)
102 .send()
103 .await
104 .map_err(|error| Error::http(error.to_string()))?;
105 if matches!(response.status().as_u16(), 301 | 302 | 303 | 307 | 308) {
106 return Ok(response
107 .headers()
108 .get(reqwest::header::LOCATION)
109 .and_then(|value| value.to_str().ok())
110 .map(str::to_string));
111 }
112 Ok(None)
113 })
114 }
115}
116
117#[derive(Clone, Debug, Default, Eq, PartialEq)]
119pub struct ProgressSummary {
120 pub downloaded_bytes: u64,
121 pub total_bytes: Option<u64>,
122 pub bytes_per_second: u64,
123 pub completed_segments: u64,
124 pub total_segments: Option<u64>,
125 pub refreshed_duration: Option<Duration>,
126 pub recorded_duration: Option<Duration>,
127 pub warnings: Vec<String>,
128 pub outputs: Vec<PathBuf>,
129 pub finished: Option<bool>,
130}
131
132#[derive(Clone, Debug, Default, Eq, PartialEq)]
134pub struct ProgressEventCollector {
135 events: Vec<ProgressEvent>,
136}
137
138impl ProgressEventCollector {
139 pub fn new() -> Self {
141 Self::default()
142 }
143
144 pub fn emit(&mut self, event: ProgressEvent) {
146 self.events.push(redact_progress_event(event));
147 }
148
149 pub fn events(&self) -> &[ProgressEvent] {
151 &self.events
152 }
153
154 pub fn summary(&self) -> ProgressSummary {
156 summarize_events(&self.events)
157 }
158}
159
160pub fn should_log(active: LogLevel, message_level: LogLevel) -> bool {
162 if active == LogLevel::Off {
163 return false;
164 }
165 rank(message_level) >= rank(active)
166}
167
168pub async fn plan_log_file(config: &LogPlanConfig) -> Result<LogFilePlan> {
170 if config.no_log {
171 return Ok(LogFilePlan {
172 enabled: false,
173 path: None,
174 });
175 }
176 let mut path = match &config.log_file_path {
177 Some(path) => path.clone(),
178 None => config
179 .default_log_dir
180 .join(format!("{}.log", config.suffix)),
181 };
182 if let Some(parent) = path.parent() {
183 tokio::fs::create_dir_all(parent).await?;
184 }
185 if tokio::fs::try_exists(&path).await? {
186 let parent = path.parent().map(Path::to_path_buf).unwrap_or_default();
187 let stem = path
188 .file_stem()
189 .and_then(|value| value.to_str())
190 .unwrap_or("log")
191 .to_string();
192 let extension = path
193 .extension()
194 .and_then(|value| value.to_str())
195 .unwrap_or("log")
196 .to_string();
197 let mut index = 1_u32;
198 loop {
199 let candidate = parent.join(format!("{stem}-{index}.{extension}"));
200 if !tokio::fs::try_exists(&candidate).await? {
201 path = candidate;
202 break;
203 }
204 index += 1;
205 }
206 }
207 Ok(LogFilePlan {
208 enabled: true,
209 path: Some(path),
210 })
211}
212
213pub async fn initialize_log_file(
215 config: &LogPlanConfig,
216 started_at: &str,
217 command_line: Option<&str>,
218) -> Result<LogFilePlan> {
219 let plan = plan_log_file(config).await?;
220 let Some(path) = &plan.path else {
221 return Ok(plan);
222 };
223 let parent = path.parent().map(Path::to_path_buf).unwrap_or_default();
224 let mut text = String::new();
225 text.push_str("LOG ");
226 text.push_str(started_at.split_whitespace().next().unwrap_or(started_at));
227 text.push('\n');
228 text.push_str("Save Path: ");
229 text.push_str(&parent.display().to_string());
230 text.push('\n');
231 text.push_str("Task Start: ");
232 text.push_str(started_at);
233 text.push('\n');
234 if let Some(command_line) = command_line {
235 text.push_str("Task CommandLine: ");
236 text.push_str(command_line);
237 text.push('\n');
238 }
239 text.push('\n');
240 tokio::fs::write(path, text).await?;
241 Ok(plan)
242}
243
244pub async fn append_log_file(plan: &LogFilePlan, line: &str) -> Result<()> {
246 let Some(path) = &plan.path else {
247 return Ok(());
248 };
249 use tokio::io::AsyncWriteExt;
250
251 let mut file = tokio::fs::OpenOptions::new()
252 .append(true)
253 .open(path)
254 .await?;
255 file.write_all(line.as_bytes()).await?;
256 file.write_all(b"\n").await?;
257 Ok(())
258}
259
260pub async fn check_update_with_client(
262 client: &impl UpdateCheckClient,
263 endpoint: &str,
264 current_version: &str,
265) -> Result<UpdateCheckResult> {
266 let Some(location) = client.redirect_location(endpoint).await? else {
267 return Ok(UpdateCheckResult::default());
268 };
269 let latest_version = latest_version_from_release_redirect(&location);
270 let update_available = latest_version
271 .as_deref()
272 .is_some_and(|latest| normalize_version(latest) != normalize_version(current_version));
273 Ok(UpdateCheckResult {
274 latest_version,
275 update_available,
276 })
277}
278
279pub fn latest_version_from_release_redirect(location: &str) -> Option<String> {
281 let marker = "/tag/";
282 let index = location.find(marker)?;
283 let tag = location.get(index + marker.len()..)?.trim();
284 if tag.is_empty() || tag.starts_with("http") {
285 return None;
286 }
287 Some(tag.to_string())
288}
289
290pub fn spawn_update_check_if_enabled(disabled: bool) -> bool {
292 if disabled {
293 return false;
294 }
295 tokio::spawn(async {
296 let client = UpdateCheckHttpClient::new();
297 let _ =
298 check_update_with_client(&client, DEFAULT_UPDATE_CHECK_URL, env!("CARGO_PKG_VERSION"))
299 .await;
300 });
301 true
302}
303
304pub fn format_file_size(bytes: u64) -> String {
306 let units = ["B", "KiB", "MiB", "GiB", "TiB"];
307 let mut value = bytes as f64;
308 let mut unit = units[0];
309 for candidate in units.iter().skip(1) {
310 if value < 1024.0 {
311 break;
312 }
313 value /= 1024.0;
314 unit = candidate;
315 }
316 if unit == "B" {
317 format!("{bytes}B")
318 } else {
319 format!("{value:.2}{unit}")
320 }
321}
322
323fn normalize_version(version: &str) -> &str {
324 version.trim().trim_start_matches('v')
325}
326
327pub fn format_duration(duration: Duration) -> String {
329 let seconds = duration.as_secs();
330 let hours = seconds / 3600;
331 let minutes = (seconds / 60) % 60;
332 let seconds = seconds % 60;
333 format!("{hours:02}:{minutes:02}:{seconds:02}")
334}
335
336pub fn redact_progress_event(event: ProgressEvent) -> ProgressEvent {
338 match event {
339 ProgressEvent::Log { level, message } => ProgressEvent::Log {
340 level,
341 message: redact_secrets(&message),
342 },
343 ProgressEvent::ExtraLog { message } => ProgressEvent::ExtraLog {
344 message: redact_secrets(&message),
345 },
346 ProgressEvent::MediaInfo { stream_id, lines } => ProgressEvent::MediaInfo {
347 stream_id,
348 lines: lines
349 .into_iter()
350 .map(|line| redact_secrets(&line))
351 .collect(),
352 },
353 ProgressEvent::Warning { message } => ProgressEvent::Warning {
354 message: redact_secrets(&message),
355 },
356 ProgressEvent::DecryptProgress { stream_id, message } => ProgressEvent::DecryptProgress {
357 stream_id,
358 message: redact_secrets(&message),
359 },
360 ProgressEvent::MergeProgress { stream_id, message } => ProgressEvent::MergeProgress {
361 stream_id,
362 message: redact_secrets(&message),
363 },
364 ProgressEvent::SubtitleProgress { stream_id, message } => ProgressEvent::SubtitleProgress {
365 stream_id,
366 message: redact_secrets(&message),
367 },
368 ProgressEvent::MuxProgress { message } => ProgressEvent::MuxProgress {
369 message: redact_secrets(&message),
370 },
371 ProgressEvent::ExternalToolOutput { message } => ProgressEvent::ExternalToolOutput {
372 message: redact_secrets(&message),
373 },
374 ProgressEvent::ConsoleLine { message } => ProgressEvent::ConsoleLine {
375 message: redact_secrets(&message),
376 },
377 other => other,
378 }
379}
380
381pub fn summarize_events(events: &[ProgressEvent]) -> ProgressSummary {
383 let mut summary = ProgressSummary::default();
384 for event in events {
385 match event {
386 ProgressEvent::AggregateProgress(progress) => {
387 summary.downloaded_bytes = progress.downloaded_bytes;
388 summary.total_bytes = progress.total_bytes;
389 summary.bytes_per_second = progress.bytes_per_second;
390 }
391 ProgressEvent::StreamProgress(progress) => {
392 summary.downloaded_bytes = summary.downloaded_bytes.max(progress.downloaded_bytes);
393 summary.completed_segments =
394 summary.completed_segments.max(progress.completed_segments);
395 summary.total_segments = progress.total_segments;
396 summary.bytes_per_second = summary.bytes_per_second.max(progress.bytes_per_second);
397 }
398 ProgressEvent::LiveRefresh {
399 refreshed_duration,
400 recorded_duration,
401 ..
402 } => {
403 summary.refreshed_duration = Some(*refreshed_duration);
404 summary.recorded_duration = Some(*recorded_duration);
405 }
406 ProgressEvent::Warning { message } => summary.warnings.push(message.clone()),
407 ProgressEvent::ExternalToolOutput { message } => summary.warnings.push(message.clone()),
408 ProgressEvent::ConsoleLine { message } => summary.warnings.push(message.clone()),
409 ProgressEvent::OutputArtifact(artifact) => summary.outputs.push(artifact.path.clone()),
410 ProgressEvent::Finished { success } => summary.finished = Some(*success),
411 _ => {}
412 }
413 }
414 summary
415}
416
417pub fn streams_metadata_json(streams: &[Stream]) -> String {
419 if streams.is_empty() {
420 return "[]".to_string();
421 }
422 let mut output = String::from("[\n");
423 for (index, stream) in streams.iter().enumerate() {
424 if index > 0 {
425 output.push_str(",\n");
426 }
427 output.push_str(" ");
428 output.push_str(&stream_metadata_json(stream, 2));
429 }
430 output.push_str("\n]");
431 n_json_line_endings(output)
432}
433
434pub async fn write_metadata_jsons(
436 directory: &Path,
437 raw_streams: &[Stream],
438 selected_streams: &[Stream],
439) -> Result<Vec<PathBuf>> {
440 tokio::fs::create_dir_all(directory).await?;
441 let raw = directory.join("meta.json");
442 let selected = directory.join("meta_selected.json");
443 write_n_utf8_text_file(&raw, &streams_metadata_json(raw_streams)).await?;
444 write_n_utf8_text_file(&selected, &streams_metadata_json(selected_streams)).await?;
445 Ok(vec![raw, selected])
446}
447
448fn stream_metadata_json(stream: &Stream, indent: usize) -> String {
449 let mut fields = Vec::new();
450 push_option_enum(
451 &mut fields,
452 "MediaType",
453 stream.media_type.map(media_type_text),
454 );
455 push_option_string(&mut fields, "GroupId", stream.group_id.as_deref());
456 push_option_string(&mut fields, "Language", stream.language.as_deref());
457 push_option_string(&mut fields, "Name", stream.name.as_deref());
458 push_option_enum(&mut fields, "Default", stream.default.map(choice_text));
459 push_option_number(&mut fields, "SkippedDuration", stream.skipped_duration);
460 if let Some(data) = &stream.mss_data {
461 fields.push(("MSSData".to_string(), mss_data_json(data, indent + 2)));
462 }
463 push_option_number(&mut fields, "Bandwidth", stream.bandwidth);
464 push_option_string(&mut fields, "Codecs", stream.codecs.as_deref());
465 push_option_string(&mut fields, "Resolution", stream.resolution.as_deref());
466 push_option_number(&mut fields, "FrameRate", stream.frame_rate);
467 push_option_string(&mut fields, "Channels", stream.channels.as_deref());
468 push_option_string(&mut fields, "Extension", stream.extension.as_deref());
469 if let Some(role) = stream.role {
470 fields.push(("Role".to_string(), json_string(&role_text(role))));
471 }
472 push_option_string(&mut fields, "VideoRange", stream.video_range.as_deref());
473 push_option_string(
474 &mut fields,
475 "Characteristics",
476 stream.characteristics.as_deref(),
477 );
478 push_option_string(&mut fields, "PublishTime", stream.publish_time.as_deref());
479 push_option_string(&mut fields, "AudioId", stream.audio_id.as_deref());
480 push_option_string(&mut fields, "VideoId", stream.video_id.as_deref());
481 push_option_string(&mut fields, "SubtitleId", stream.subtitle_id.as_deref());
482 push_option_string(&mut fields, "PeriodId", stream.period_id.as_deref());
483 fields.push((
484 "Url".to_string(),
485 json_string(&metadata_stream_url(&stream.url)),
486 ));
487 fields.push((
488 "OriginalUrl".to_string(),
489 json_string(&metadata_stream_url(&stream.original_url)),
490 ));
491 if let Some(playlist) = &stream.playlist {
492 fields.push(("Playlist".to_string(), playlist_json(playlist, indent + 2)));
493 }
494 fields.push((
495 "SegmentsCount".to_string(),
496 stream.segments_count().to_string(),
497 ));
498 object_json(&fields, indent)
499}
500
501fn playlist_json(playlist: &Playlist, indent: usize) -> String {
502 let mut fields = vec![
503 ("Url".to_string(), json_string("")),
504 ("IsLive".to_string(), playlist.is_live.to_string()),
505 (
506 "RefreshIntervalMs".to_string(),
507 number_json(playlist.refresh_interval_ms),
508 ),
509 (
510 "TotalDuration".to_string(),
511 number_json(playlist.total_duration()),
512 ),
513 ];
514 push_option_number(&mut fields, "TargetDuration", playlist.target_duration);
515 if let Some(init) = &playlist.media_init {
516 fields.push(("MediaInit".to_string(), segment_json(init, indent + 2)));
517 }
518 fields.push((
519 "MediaParts".to_string(),
520 array_json(
521 &playlist
522 .media_parts
523 .iter()
524 .map(|part| media_part_json(part, indent + 4))
525 .collect::<Vec<_>>(),
526 indent + 2,
527 ),
528 ));
529 object_json(&fields, indent)
530}
531
532fn media_part_json(part: &MediaPart, indent: usize) -> String {
533 object_json(
534 &[(
535 "MediaSegments".to_string(),
536 array_json(
537 &part
538 .media_segments
539 .iter()
540 .map(|segment| segment_json(segment, indent + 4))
541 .collect::<Vec<_>>(),
542 indent + 2,
543 ),
544 )],
545 indent,
546 )
547}
548
549fn segment_json(segment: &MediaSegment, indent: usize) -> String {
550 let mut fields = vec![
551 ("Index".to_string(), segment.index.to_string()),
552 ("Duration".to_string(), number_json(segment.duration)),
553 ];
554 push_option_string(&mut fields, "Title", segment.title.as_deref());
555 push_option_string(
556 &mut fields,
557 "DateTime",
558 segment.program_date_time.as_deref(),
559 );
560 push_option_number(&mut fields, "StartRange", segment.start_range);
561 push_option_number(&mut fields, "StopRange", segment.stop_range());
562 push_option_number(&mut fields, "ExpectLength", segment.expected_length);
563 fields.push((
564 "EncryptInfo".to_string(),
565 encryption_json(&segment.encryption, indent + 2),
566 ));
567 fields.push((
568 "IsEncrypted".to_string(),
569 segment.is_encrypted().to_string(),
570 ));
571 fields.push((
572 "Url".to_string(),
573 json_string(&metadata_segment_url(&segment.url)),
574 ));
575 push_option_string(&mut fields, "NameFromVar", segment.name_from_var.as_deref());
576 object_json(&fields, indent)
577}
578
579fn encryption_json(encryption: &EncryptionInfo, indent: usize) -> String {
580 let mut fields = vec![(
581 "Method".to_string(),
582 json_string(encryption_method_text(encryption.method)),
583 )];
584 push_option_bytes(&mut fields, "Key", encryption.key.as_deref());
585 push_option_bytes(&mut fields, "IV", encryption.iv.as_deref());
586 object_json(&fields, indent)
587}
588
589fn mss_data_json(data: &MssData, indent: usize) -> String {
590 object_json(
591 &[
592 ("FourCC".to_string(), json_string(&data.four_cc)),
593 (
594 "CodecPrivateData".to_string(),
595 json_string(&data.codec_private_data),
596 ),
597 ("Type".to_string(), json_string(&data.stream_type)),
598 ("Timesacle".to_string(), data.timescale.to_string()),
599 ("SamplingRate".to_string(), data.sampling_rate.to_string()),
600 ("Channels".to_string(), data.channels.to_string()),
601 (
602 "BitsPerSample".to_string(),
603 data.bits_per_sample.to_string(),
604 ),
605 (
606 "NalUnitLengthField".to_string(),
607 data.nal_unit_length_field.to_string(),
608 ),
609 ("Duration".to_string(), data.duration.to_string()),
610 ("IsProtection".to_string(), data.is_protection.to_string()),
611 (
612 "ProtectionSystemID".to_string(),
613 json_string(&data.protection_system_id),
614 ),
615 (
616 "ProtectionData".to_string(),
617 json_string(&data.protection_data),
618 ),
619 ],
620 indent,
621 )
622}
623
624fn metadata_stream_url(value: &str) -> String {
625 value.replace(' ', "%20")
626}
627
628fn metadata_segment_url(value: &str) -> String {
629 value.replace("%20", " ")
630}
631
632fn n_json_line_endings(value: String) -> String {
633 if cfg!(windows) {
634 value.replace('\n', "\r\n")
635 } else {
636 value
637 }
638}
639
640async fn write_n_utf8_text_file(path: &Path, text: &str) -> std::io::Result<()> {
641 let mut bytes = Vec::with_capacity(3 + text.len());
642 bytes.extend_from_slice(&[0xef, 0xbb, 0xbf]);
643 bytes.extend_from_slice(text.as_bytes());
644 tokio::fs::write(path, bytes).await
645}
646
647fn object_json(fields: &[(String, String)], indent: usize) -> String {
648 if fields.is_empty() {
649 return "{}".to_string();
650 }
651 let current = " ".repeat(indent);
652 let child = " ".repeat(indent + 2);
653 let mut output = String::from("{\n");
654 for (index, (name, value)) in fields.iter().enumerate() {
655 if index > 0 {
656 output.push_str(",\n");
657 }
658 output.push_str(&child);
659 output.push_str(&json_string(name));
660 output.push_str(": ");
661 output.push_str(value);
662 }
663 output.push('\n');
664 output.push_str(¤t);
665 output.push('}');
666 output
667}
668
669fn array_json(values: &[String], indent: usize) -> String {
670 if values.is_empty() {
671 return "[]".to_string();
672 }
673 let current = " ".repeat(indent);
674 let child = " ".repeat(indent + 2);
675 let mut output = String::from("[\n");
676 for (index, value) in values.iter().enumerate() {
677 if index > 0 {
678 output.push_str(",\n");
679 }
680 output.push_str(&child);
681 output.push_str(value);
682 }
683 output.push('\n');
684 output.push_str(¤t);
685 output.push(']');
686 output
687}
688
689fn push_option_string(fields: &mut Vec<(String, String)>, name: &str, value: Option<&str>) {
690 if let Some(value) = value {
691 fields.push((name.to_string(), json_string(value)));
692 }
693}
694
695fn push_option_enum(fields: &mut Vec<(String, String)>, name: &str, value: Option<&str>) {
696 if let Some(value) = value {
697 fields.push((name.to_string(), json_string(value)));
698 }
699}
700
701fn push_option_number<T: ToString>(
702 fields: &mut Vec<(String, String)>,
703 name: &str,
704 value: Option<T>,
705) {
706 if let Some(value) = value {
707 fields.push((name.to_string(), value.to_string()));
708 }
709}
710
711fn push_option_bytes(fields: &mut Vec<(String, String)>, name: &str, value: Option<&[u8]>) {
712 if let Some(value) = value {
713 fields.push((name.to_string(), json_string(&base64_encode(value))));
714 }
715}
716
717fn media_type_text(value: MediaType) -> &'static str {
718 match value {
719 MediaType::Audio => "AUDIO",
720 MediaType::Video => "VIDEO",
721 MediaType::Subtitles => "SUBTITLES",
722 MediaType::ClosedCaptions => "CLOSED_CAPTIONS",
723 }
724}
725
726fn role_text(value: RoleType) -> String {
727 match value {
728 RoleType::Subtitle => "Subtitle".to_string(),
729 RoleType::Main => "Main".to_string(),
730 RoleType::Alternate => "Alternate".to_string(),
731 RoleType::Supplementary => "Supplementary".to_string(),
732 RoleType::Commentary => "Commentary".to_string(),
733 RoleType::Dub => "Dub".to_string(),
734 RoleType::Description => "Description".to_string(),
735 RoleType::Sign => "Sign".to_string(),
736 RoleType::Metadata => "Metadata".to_string(),
737 RoleType::ForcedSubtitle => "ForcedSubtitle".to_string(),
738 RoleType::Numeric(value) => value.to_string(),
739 }
740}
741
742fn choice_text(value: Choice) -> &'static str {
743 match value {
744 Choice::No => "NO",
745 Choice::Yes => "YES",
746 }
747}
748
749fn encryption_method_text(value: EncryptionMethod) -> &'static str {
750 match value {
751 EncryptionMethod::None => "NONE",
752 EncryptionMethod::Aes128 => "AES_128",
753 EncryptionMethod::Aes128Ecb => "AES_128_ECB",
754 EncryptionMethod::SampleAes => "SAMPLE_AES",
755 EncryptionMethod::SampleAesCtr => "SAMPLE_AES_CTR",
756 EncryptionMethod::Cenc => "CENC",
757 EncryptionMethod::Chacha20 => "CHACHA20",
758 EncryptionMethod::Unknown => "UNKNOWN",
759 }
760}
761
762fn number_json(value: f64) -> String {
763 value.to_string()
764}
765
766fn json_string(value: &str) -> String {
767 format!("\"{}\"", escape_json(value))
768}
769
770fn escape_json(value: &str) -> String {
771 let mut output = String::new();
772 for ch in value.chars() {
773 match ch {
774 '"' => output.push_str("\\\""),
775 '\\' => output.push_str("\\\\"),
776 '\n' => output.push_str("\\n"),
777 '\r' => output.push_str("\\r"),
778 '\t' => output.push_str("\\t"),
779 _ => output.push(ch),
780 }
781 }
782 output
783}
784
785fn base64_encode(bytes: &[u8]) -> String {
786 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
787 let mut output = String::new();
788 for chunk in bytes.chunks(3) {
789 let b0 = chunk[0];
790 let b1 = *chunk.get(1).unwrap_or(&0);
791 let b2 = *chunk.get(2).unwrap_or(&0);
792 let triple = (u32::from(b0) << 16) | (u32::from(b1) << 8) | u32::from(b2);
793 output.push(char::from(TABLE[((triple >> 18) & 0x3f) as usize]));
794 output.push(char::from(TABLE[((triple >> 12) & 0x3f) as usize]));
795 if chunk.len() > 1 {
796 output.push(char::from(TABLE[((triple >> 6) & 0x3f) as usize]));
797 } else {
798 output.push('=');
799 }
800 if chunk.len() > 2 {
801 output.push(char::from(TABLE[(triple & 0x3f) as usize]));
802 } else {
803 output.push('=');
804 }
805 }
806 output
807}
808
809fn rank(level: LogLevel) -> u8 {
810 match level {
811 LogLevel::Debug => 0,
812 LogLevel::Info => 1,
813 LogLevel::Warn => 2,
814 LogLevel::Error => 3,
815 LogLevel::Off => 4,
816 }
817}