-
Notifications
You must be signed in to change notification settings - Fork 0
fix(audio): establish canonical local-audio resource policy #866
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
Draft
seonghobae
wants to merge
86
commits into
develop
Choose a base branch
from
fix/audio-resource-policy-781
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,693
−123
Draft
Changes from all commits
Commits
Show all changes
86 commits
Select commit
Hold shift + click to select a range
cb251f7
test(audio): define canonical resource policy contract
seonghobae 3e96ed6
feat(audio): add canonical resource policy
seonghobae f83a1ba
test(score): require bounded validated PDF reads
seonghobae d659d9d
refactor(core): expose bounded score reader module
seonghobae fc1af87
refactor(core): preserve public API through root module
seonghobae 1df5fe0
fix(score): bound stored PDF reads before allocation
seonghobae 521fe12
fix(score): use bounded native PDF reader
seonghobae ca20dc5
style(score): keep bounded reader rustfmt-clean
seonghobae e6d31ee
docs(changelog): record bounded stored-score reads
seonghobae 051e39d
fix(core): preserve desktop-core package contract
seonghobae c11af77
test(score): cover non-file stored score reads
seonghobae f86e266
test(score): prove same-descriptor growth detection
seonghobae 6aa0098
test(audio): require policy parity at orchestration and decode bounda…
seonghobae ee3b48c
fix(audio): add one-sample decode probe to resource policy
seonghobae a982e7f
fix(audio): bind temporal decode to canonical resource policy
seonghobae 38b3fb6
fix(audio): enforce canonical policy before stem inference
seonghobae ed6e4f7
fix(audio): apply canonical byte policy at request preflight
seonghobae e7671de
docs(audio): record resource-boundary evidence
seonghobae 9549647
docs(changelog): record canonical audio resource bounds
seonghobae b35bc12
docs(security): bind local audio to canonical resource policy
seonghobae 710ed16
test(audio): reject resource-policy arithmetic overflow
seonghobae 6e80527
fix(audio): fail closed on extreme sample-rate arithmetic
seonghobae 572333e
style(audio): keep policy bounds formatter-clean
seonghobae adbfad0
test(audio): reject unrepresentable policy and decoder values
seonghobae 428db0a
fix(audio): make resource policy arithmetic fully fail closed
seonghobae 84f60b3
docs(audio): record checked policy arithmetic
seonghobae e42ff5c
test(audio): reject oversized desktop selection at the bridge
seonghobae 05fbbb3
test(audio): enforce encoded-byte parity for imported sources
seonghobae b23e51a
fix(audio): enforce encoded-byte parity at desktop bridge
seonghobae 509c18b
docs(changelog): record desktop audio-policy parity
seonghobae 9823e0e
fix(changelog): restore full release history after parity note drift
seonghobae f631049
fix(ci): format audio resource policy regressions
seonghobae 0ba0593
test(audio): reject fractional encoded byte metadata
seonghobae ceb7f71
fix(audio): require integral encoded byte metadata
seonghobae 84f7691
test(audio): require native encoded-byte admission
seonghobae da676cb
chore(audio): stack native resource policy on bounded core root
seonghobae 0488c8f
fix(audio): enforce native encoded-byte ceiling
seonghobae fbc7d7d
fix(audio): export native resource policy
seonghobae e61e858
fix(audio): enforce native bootstrap byte ceiling
seonghobae 3f976e5
docs(audio): record native intake enforcement
seonghobae 2b9d5e3
test(audio): require YouTube download to use canonical 100 MiB policy
cursoragent 19064f4
fix(audio): abort YouTube downloads at the canonical encoded-byte cei…
cursoragent 7b3b1e7
docs(audio): record YouTube download-time policy evidence
cursoragent d1a75c9
test(audio): cover fail-closed resource admission branches
seonghobae 1f3fdb8
merge(audio): consolidate YouTube admission repair into canonical #866
seonghobae 5e8fa77
fix(audio): delete owned YouTube partials on in-flight abort
cursoragent 6df13ae
style(audio): wrap zero-byte decoder regression
seonghobae 6d3b9b2
test(audio): reject malformed YouTube duration metadata
seonghobae 86f7272
fix(audio): reject malformed YouTube duration metadata
seonghobae bc0fc98
docs(changelog): record YouTube duration metadata guard
seonghobae ec64aa7
test(audio): bound decoded buffer memory
seonghobae 104573a
fix(audio): enforce decoded memory budget
seonghobae c6d9368
docs(audio): record decoded memory admission
seonghobae f60eb17
docs(audio): record decoded memory budget
seonghobae aeb42da
test(youtube): cover fail-closed duration conversion
seonghobae f35f1d4
test(youtube): avoid exceptional float subclasses
seonghobae f5f0c3d
test(youtube): reject noncanonical numeric metadata
seonghobae f4cee9e
fix(youtube): reject non-canonical duration numerics
seonghobae 69cdf8b
docs(changelog): record strict duration metadata type gate
seonghobae 3ea49d3
fix(youtube): remove unreachable duration conversion branch
seonghobae 97490c7
test(youtube): require post-download duration revalidation
seonghobae d58c24e
test(youtube): model owned cleanup on duration drift
seonghobae d3e2792
fix(youtube): revalidate duration after download
seonghobae 1c85058
test(privacy): fail on temporal path disclosure
seonghobae 4a6e269
fix(privacy): redact temporal analysis failure diagnostics
seonghobae 6d3c388
test(privacy): align temporal diagnostics with redaction contract
seonghobae aa0191c
test(security): reject foreign YouTube download paths
seonghobae 48d06cf
fix(security): bind completed YouTube path to import cache
seonghobae 85424c9
docs(security): record YouTube completed-path authority
seonghobae 7eca596
docs(changelog): record YouTube completed-path containment
seonghobae 6a728fd
test(audio): exercise module entrypoint with owned path
seonghobae 2b83681
style(tests): restore Ruff formatting
seonghobae 2909bc8
test(audio): reject malformed model stem output
seonghobae e5293a9
fix(audio): reject malformed model stem output
seonghobae 2464ff9
test(audio): use explicit raw error pattern
seonghobae c35d55d
test(audio): cover model conversion failures
seonghobae 15a9edb
test(audio): reproduce GPU tensor NumPy boundary
seonghobae c5519a4
fix(audio): move accelerator stems to CPU before NumPy
seonghobae 389f057
fix(audio): preserve CPU tests while bridging accelerator stems
seonghobae 2ea9e07
test(security): reproduce stem worker log disclosure
seonghobae b7dd015
test(security): cover parent stem failure logs
seonghobae cf8b62e
fix(security): redact analysis API tracebacks
seonghobae 2f281be
fix(security): keep API privacy init lint-safe
seonghobae a8bbb77
test(security): scope analysis log redaction
seonghobae 223dd78
fix(security): scope stem diagnostic redaction
seonghobae c2cc5bb
fix(audio): preflight source metadata before decode
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /// Maximum encoded local-audio file size accepted by the desktop bootstrap boundary. | ||
| pub const MAX_LOCAL_AUDIO_FILE_BYTES: u64 = 100 * 1024 * 1024; | ||
|
|
||
| const LOCAL_AUDIO_READ_ERROR: &str = "Could not read the selected audio file."; | ||
| const LOCAL_AUDIO_TOO_LARGE_ERROR: &str = | ||
| "Selected audio file exceeds the 100 MiB analysis limit."; | ||
|
|
||
| /// Validate a native local-audio file length before storing bootstrap metadata. | ||
| /// | ||
| /// The caller must obtain this length from the native filesystem descriptor or | ||
| /// metadata boundary rather than from renderer-controlled JSON. The function | ||
| /// intentionally returns only bounded product messages and never includes a | ||
| /// local path or payload content. | ||
| pub fn validate_local_audio_file_size(file_size_bytes: u64) -> Result<u64, String> { | ||
| if file_size_bytes == 0 { | ||
| return Err(LOCAL_AUDIO_READ_ERROR.to_string()); | ||
| } | ||
| if file_size_bytes > MAX_LOCAL_AUDIO_FILE_BYTES { | ||
| return Err(LOCAL_AUDIO_TOO_LARGE_ERROR.to_string()); | ||
| } | ||
| Ok(file_size_bytes) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| //! Pure, GUI-independent logic for the BandScope desktop application. | ||
| //! | ||
| //! The historical desktop-core implementation remains in `lib.rs` as the | ||
| //! compatibility module while bounded resource boundaries are isolated in | ||
| //! auditable modules. Public symbols are re-exported so downstream callers keep | ||
| //! the same crate-root API. | ||
|
|
||
| #[path = "lib.rs"] | ||
| mod runtime_core; | ||
| mod audio_resource; | ||
| mod score_pdf; | ||
|
|
||
| pub use audio_resource::{validate_local_audio_file_size, MAX_LOCAL_AUDIO_FILE_BYTES}; | ||
| pub use runtime_core::*; | ||
| pub use score_pdf::read_validated_score_pdf; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| use crate::{MAX_SCORE_PDF_BYTES, PDF_MAGIC}; | ||
| use std::{fs::File, io::Read, path::Path}; | ||
|
|
||
| const SCORE_READ_ERROR: &str = "Could not read the score PDF."; | ||
| const SCORE_TOO_LARGE_ERROR: &str = "Score PDF is too large (exceeds 25MB limit)."; | ||
| const SCORE_INVALID_PDF_ERROR: &str = "Stored score is not a valid PDF."; | ||
|
|
||
| fn read_validated_pdf_stream( | ||
| reader: &mut impl Read, | ||
| expected_len: u64, | ||
| ) -> Result<Vec<u8>, String> { | ||
| if expected_len > MAX_SCORE_PDF_BYTES { | ||
| return Err(SCORE_TOO_LARGE_ERROR.to_string()); | ||
| } | ||
|
|
||
| // MAX_SCORE_PDF_BYTES is 25 MiB, which fits every supported Rust `usize`. | ||
| let mut bytes = vec![0_u8; expected_len as usize]; | ||
| reader | ||
| .read_exact(&mut bytes) | ||
| .map_err(|_| SCORE_READ_ERROR.to_string())?; | ||
|
|
||
| let mut growth_probe = [0_u8; 1]; | ||
| if reader | ||
| .read(&mut growth_probe) | ||
| .map_err(|_| SCORE_READ_ERROR.to_string())? | ||
| != 0 | ||
| { | ||
| return Err(SCORE_TOO_LARGE_ERROR.to_string()); | ||
| } | ||
|
|
||
| if !bytes.starts_with(PDF_MAGIC) { | ||
| return Err(SCORE_INVALID_PDF_ERROR.to_string()); | ||
| } | ||
|
|
||
| Ok(bytes) | ||
| } | ||
|
|
||
| /// Read one already-authorized stored score without allocating beyond the PDF limit. | ||
| /// | ||
| /// The caller remains responsible for path authority and containment. This helper | ||
| /// opens that resolved path once, snapshots the descriptor length, allocates only | ||
| /// that bounded size, reads exactly that many bytes, and then probes one additional | ||
| /// byte on the same descriptor. A file that was already oversized is rejected | ||
| /// before heap allocation; a file that grows after metadata inspection is rejected | ||
| /// by the one-byte probe without extending the heap buffer beyond the product cap. | ||
| /// Errors intentionally omit the local path and file content. | ||
| pub fn read_validated_score_pdf(path: &Path) -> Result<Vec<u8>, String> { | ||
| let mut file = File::open(path).map_err(|_| SCORE_READ_ERROR.to_string())?; | ||
| let metadata = file | ||
| .metadata() | ||
| .map_err(|_| SCORE_READ_ERROR.to_string())?; | ||
| if !metadata.is_file() { | ||
| return Err(SCORE_READ_ERROR.to_string()); | ||
| } | ||
| read_validated_pdf_stream(&mut file, metadata.len()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use std::io::Cursor; | ||
|
|
||
| #[test] | ||
| fn stream_rejects_growth_after_the_metadata_length_snapshot() { | ||
| let mut reader = Cursor::new(b"%PDF-extra".to_vec()); | ||
|
|
||
| let error = read_validated_pdf_stream(&mut reader, PDF_MAGIC.len() as u64) | ||
| .expect_err("bytes beyond the metadata snapshot must fail closed"); | ||
|
|
||
| assert_eq!(error, SCORE_TOO_LARGE_ERROR); | ||
| } | ||
|
|
||
| #[test] | ||
| fn stream_rejects_truncation_after_the_metadata_length_snapshot() { | ||
| let mut reader = Cursor::new(PDF_MAGIC.to_vec()); | ||
|
|
||
| let error = read_validated_pdf_stream(&mut reader, (PDF_MAGIC.len() + 1) as u64) | ||
| .expect_err("truncation after the metadata snapshot must fail closed"); | ||
|
|
||
| assert_eq!(error, SCORE_READ_ERROR); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| use bandscope_desktop_core::{validate_local_audio_file_size, MAX_LOCAL_AUDIO_FILE_BYTES}; | ||
|
|
||
| #[test] | ||
| fn local_audio_size_policy_accepts_the_exact_native_bootstrap_ceiling() { | ||
| assert_eq!( | ||
| validate_local_audio_file_size(MAX_LOCAL_AUDIO_FILE_BYTES), | ||
| Ok(MAX_LOCAL_AUDIO_FILE_BYTES) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn local_audio_size_policy_rejects_an_empty_native_bootstrap_source() { | ||
| assert_eq!( | ||
| validate_local_audio_file_size(0), | ||
| Err("Could not read the selected audio file.".to_string()) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn local_audio_size_policy_rejects_a_native_source_above_the_canonical_ceiling() { | ||
| assert_eq!( | ||
| validate_local_audio_file_size(MAX_LOCAL_AUDIO_FILE_BYTES + 1), | ||
| Err("Selected audio file exceeds the 100 MiB analysis limit.".to_string()) | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| use bandscope_desktop_core::{read_validated_score_pdf, MAX_SCORE_PDF_BYTES}; | ||
| use std::io::Write; | ||
| use std::path::PathBuf; | ||
| use std::time::{SystemTime, UNIX_EPOCH}; | ||
|
|
||
| fn unique_test_dir(name: &str) -> PathBuf { | ||
| let suffix = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("system clock should be after epoch") | ||
| .as_nanos(); | ||
| std::env::temp_dir().join(format!("bandscope-{name}-{suffix}")) | ||
| } | ||
|
|
||
| #[test] | ||
| fn score_pdf_read_returns_only_valid_bounded_pdf_bytes() { | ||
| let root = unique_test_dir("score-read-valid"); | ||
| std::fs::create_dir_all(&root).expect("test directory should be created"); | ||
| let path = root.join("score.pdf"); | ||
| let expected = b"%PDF-1.7\nvalidated body"; | ||
| std::fs::write(&path, expected).expect("valid PDF fixture should be written"); | ||
|
|
||
| let actual = read_validated_score_pdf(&path).expect("valid stored PDF should be readable"); | ||
|
|
||
| assert_eq!(actual, expected); | ||
| let _ = std::fs::remove_dir_all(root); | ||
| } | ||
|
|
||
| #[test] | ||
| fn score_pdf_read_rejects_empty_short_and_wrong_magic_content() { | ||
| let root = unique_test_dir("score-read-invalid"); | ||
| std::fs::create_dir_all(&root).expect("test directory should be created"); | ||
|
|
||
| for (name, content) in [ | ||
| ("empty.pdf", b"".as_slice()), | ||
| ("short.pdf", b"%PD".as_slice()), | ||
| ("wrong.pdf", b"PK\x03\x04 not a PDF".as_slice()), | ||
| ] { | ||
| let path = root.join(name); | ||
| std::fs::write(&path, content).expect("invalid PDF fixture should be written"); | ||
| let error = read_validated_score_pdf(&path).expect_err("invalid PDF must fail closed"); | ||
| assert!( | ||
| error == "Could not read the score PDF." || error == "Stored score is not a valid PDF.", | ||
| "unexpected payload-safe error: {error}" | ||
| ); | ||
| assert!(!error.contains(root.to_string_lossy().as_ref())); | ||
| } | ||
|
|
||
| let _ = std::fs::remove_dir_all(root); | ||
| } | ||
|
|
||
| #[test] | ||
| fn score_pdf_read_rejects_oversized_sparse_file_before_heap_allocation() { | ||
| let root = unique_test_dir("score-read-oversized"); | ||
| std::fs::create_dir_all(&root).expect("test directory should be created"); | ||
| let path = root.join("oversized.pdf"); | ||
| let mut file = std::fs::File::create(&path).expect("oversized PDF fixture should be created"); | ||
| file.write_all(b"%PDF-") | ||
| .expect("PDF magic should be written before extending sparse file"); | ||
| file.set_len(MAX_SCORE_PDF_BYTES + 1) | ||
| .expect("sparse PDF fixture should exceed the product limit"); | ||
| drop(file); | ||
|
|
||
| let error = read_validated_score_pdf(&path).expect_err("oversized PDF must fail closed"); | ||
|
|
||
| assert_eq!(error, "Score PDF is too large (exceeds 25MB limit)."); | ||
| let _ = std::fs::remove_dir_all(root); | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| #[test] | ||
| fn score_pdf_read_rejects_non_file_descriptor() { | ||
| let root = unique_test_dir("score-read-directory"); | ||
| std::fs::create_dir_all(&root).expect("test directory should be created"); | ||
|
|
||
| let error = read_validated_score_pdf(&root).expect_err("directory must fail closed"); | ||
|
|
||
| assert_eq!(error, "Could not read the score PDF."); | ||
| assert!(!error.contains(root.to_string_lossy().as_ref())); | ||
| let _ = std::fs::remove_dir_all(root); | ||
| } | ||
|
|
||
| #[test] | ||
| fn score_pdf_read_rejects_missing_file_without_exposing_path() { | ||
| let root = unique_test_dir("score-read-missing"); | ||
| let path = root.join("private-score.pdf"); | ||
|
|
||
| let error = read_validated_score_pdf(&path).expect_err("missing PDF must fail closed"); | ||
|
|
||
| assert_eq!(error, "Could not read the score PDF."); | ||
| assert!(!error.contains("private-score.pdf")); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { importYoutubeUrl, selectLocalAudioSource } from "./analysis"; | ||
|
|
||
| type TauriWindow = Window & { | ||
| __TAURI_INTERNALS__?: unknown; | ||
| __TAURI_INVOKE__?: unknown; | ||
| }; | ||
|
|
||
| const tauriWindow = window as TauriWindow; | ||
| const INVALID_RESOURCE_POLICY_MESSAGE = | ||
| "Selected audio file metadata violates the analysis resource policy."; | ||
|
|
||
| function fractionalBootstrap(projectId: string) { | ||
| return { | ||
| projectId, | ||
| sourceMode: "reference", | ||
| projectRoot: `/tmp/bandscope/projects/${projectId}`, | ||
| cacheRoot: `/tmp/bandscope/cache/${projectId}`, | ||
| tempRoot: `/tmp/bandscope/temp/${projectId}`, | ||
| source: { | ||
| sourcePath: `/tmp/bandscope/${projectId}/input.wav`, | ||
| fileName: "input.wav", | ||
| extension: "wav", | ||
| fileSizeBytes: 1.5 | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| describe("analysis encoded-byte policy parity", () => { | ||
| beforeEach(() => { | ||
| delete tauriWindow.__TAURI_INTERNALS__; | ||
| delete tauriWindow.__TAURI_INVOKE__; | ||
| }); | ||
|
|
||
| it("rejects fractional local-file metadata before project state", async () => { | ||
| tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue(fractionalBootstrap("local-project")); | ||
|
|
||
| await expect(selectLocalAudioSource()).resolves.toEqual({ | ||
| ok: false, | ||
| error: { | ||
| code: "invalid_request", | ||
| message: INVALID_RESOURCE_POLICY_MESSAGE | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| it("rejects fractional imported-file metadata before project state", async () => { | ||
| tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue(fractionalBootstrap("youtube-project")); | ||
|
|
||
| await expect(importYoutubeUrl("https://youtu.be/4ozX4yFUC34")).resolves.toEqual({ | ||
| ok: false, | ||
| error: { | ||
| code: "invalid_request", | ||
| message: INVALID_RESOURCE_POLICY_MESSAGE | ||
| } | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.