mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-10-12 01:24:27 +08:00
Add macos route and some other tests I guess
This commit is contained in:
@@ -350,7 +350,10 @@ pub fn get_browse_cards() -> JsonValue {
|
|||||||
// considered, and ids are never reused, so official (or imported) cards can't
|
// considered, and ids are never reused, so official (or imported) cards can't
|
||||||
// come back from this and a wipe is final. A card that's merely unpublished
|
// come back from this and a wipe is final. A card that's merely unpublished
|
||||||
// still has its row - only genuinely deleted ids are returned
|
// still has its row - only genuinely deleted ids are returned
|
||||||
pub fn dead_card_ids(candidates: &JsonValue) -> JsonValue {
|
// None = the catalog could not be read (or is entirely empty while players still hold
|
||||||
|
// custom ids): the caller must prune NOTHING then. An unreadable catalog looks exactly
|
||||||
|
// like one where every id is dead, and get_acc saves the pruned userdata.
|
||||||
|
pub fn dead_card_ids(candidates: &JsonValue) -> Option<JsonValue> {
|
||||||
let mut ids: Vec<i64> = Vec::new();
|
let mut ids: Vec<i64> = Vec::new();
|
||||||
for id in candidates.members() {
|
for id in candidates.members() {
|
||||||
let Some(id) = id.as_i64() else { continue; };
|
let Some(id) = id.as_i64() else { continue; };
|
||||||
@@ -359,17 +362,23 @@ pub fn dead_card_ids(candidates: &JsonValue) -> JsonValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return array![];
|
return Some(array![]);
|
||||||
}
|
}
|
||||||
let list = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
let list = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
||||||
let alive = DATABASE.lock_and_select_all(&format!("SELECT master_card_id FROM cards WHERE master_card_id IN ({})", list), params!()).unwrap_or(array![]);
|
let alive = DATABASE.lock_and_select_all(&format!("SELECT master_card_id FROM cards WHERE master_card_id IN ({})", list), params!()).ok()?;
|
||||||
|
if alive.is_empty() {
|
||||||
|
let total: i64 = DATABASE.lock_and_select_type("SELECT COUNT(*) FROM cards", params!()).ok()?;
|
||||||
|
if total == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut rv = array![];
|
let mut rv = array![];
|
||||||
for id in ids {
|
for id in ids {
|
||||||
if !alive.contains(id) {
|
if !alive.contains(id) {
|
||||||
rv.push(id).unwrap();
|
rv.push(id).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rv
|
Some(rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The published + obtainable pool the custom gacha banner draws from, per
|
// The published + obtainable pool the custom gacha banner draws from, per
|
||||||
@@ -596,10 +605,33 @@ mod tests {
|
|||||||
insert_card(dead, 1001, 3008, &card_blob(dead, 1), true, false).unwrap();
|
insert_card(dead, 1001, 3008, &card_blob(dead, 1), true, false).unwrap();
|
||||||
delete_card(dead);
|
delete_card(dead);
|
||||||
|
|
||||||
let dead_ids = dead_card_ids(&array![alive, dead, 10010001, 100010001, dead]);
|
let dead_ids = dead_card_ids(&array![alive, dead, 10010001, 100010001, dead]).unwrap();
|
||||||
assert_eq!(dead_ids.len(), 1);
|
assert_eq!(dead_ids.len(), 1);
|
||||||
assert_eq!(dead_ids[0].as_i64(), Some(dead));
|
assert_eq!(dead_ids[0].as_i64(), Some(dead));
|
||||||
|
|
||||||
wipe(3008);
|
wipe(3008);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An unreadable or blank catalog must never read as "every custom card is
|
||||||
|
// dead": get_acc prunes on that answer and saves the pruned userdata
|
||||||
|
#[test]
|
||||||
|
fn dead_ids_are_unknown_when_the_catalog_cannot_be_read() {
|
||||||
|
let _lock = crate::runtime::lock_test_data_path();
|
||||||
|
wipe(3009);
|
||||||
|
let alive = next_card_id();
|
||||||
|
insert_card(alive, 1001, 3009, &card_blob(alive, 1), false, false).unwrap();
|
||||||
|
|
||||||
|
let conn = rusqlite::Connection::open(DATABASE.get_path()).unwrap();
|
||||||
|
conn.execute("ALTER TABLE cards RENAME TO cards_hidden", ()).unwrap();
|
||||||
|
let unreadable = dead_card_ids(&array![alive]);
|
||||||
|
conn.execute("CREATE TABLE cards (master_card_id BIGINT NOT NULL PRIMARY KEY, master_character_id BIGINT NOT NULL, owner_id BIGINT NOT NULL, card TEXT NOT NULL, rarity INT NOT NULL DEFAULT 1, published INT NOT NULL DEFAULT 0, obtainable INT NOT NULL DEFAULT 0)", ()).unwrap();
|
||||||
|
let blank = dead_card_ids(&array![alive]);
|
||||||
|
conn.execute("DROP TABLE cards", ()).unwrap();
|
||||||
|
conn.execute("ALTER TABLE cards_hidden RENAME TO cards", ()).unwrap();
|
||||||
|
|
||||||
|
assert!(unreadable.is_none(), "an unreadable catalog reported dead cards");
|
||||||
|
assert!(blank.is_none(), "a blank catalog reported every card dead");
|
||||||
|
assert_eq!(dead_card_ids(&array![alive]).unwrap().len(), 0);
|
||||||
|
wipe(3009);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,7 +365,10 @@ pub fn non_public_music_ids_for(user_id: i64) -> JsonValue {
|
|||||||
// range is ever considered, so official songs can't come back from this. A song
|
// range is ever considered, so official songs can't come back from this. A song
|
||||||
// that's merely private/shared still has its row - only genuinely deleted ids
|
// that's merely private/shared still has its row - only genuinely deleted ids
|
||||||
// (which are never reused) are returned
|
// (which are never reused) are returned
|
||||||
pub fn dead_music_ids(candidates: &JsonValue) -> JsonValue {
|
// None = the catalog could not be read (or is entirely empty while players still hold
|
||||||
|
// custom ids): the caller must prune NOTHING then. An unreadable catalog looks exactly
|
||||||
|
// like one where every id is dead, and get_acc saves the pruned userdata.
|
||||||
|
pub fn dead_music_ids(candidates: &JsonValue) -> Option<JsonValue> {
|
||||||
let mut ids: Vec<i64> = Vec::new();
|
let mut ids: Vec<i64> = Vec::new();
|
||||||
for id in candidates.members() {
|
for id in candidates.members() {
|
||||||
let Some(id) = id.as_i64() else { continue; };
|
let Some(id) = id.as_i64() else { continue; };
|
||||||
@@ -374,17 +377,23 @@ pub fn dead_music_ids(candidates: &JsonValue) -> JsonValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return array![];
|
return Some(array![]);
|
||||||
}
|
}
|
||||||
let list = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
let list = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
||||||
let alive = DATABASE.lock_and_select_all(&format!("SELECT music_id FROM songs WHERE music_id IN ({})", list), params!()).unwrap_or(array![]);
|
let alive = DATABASE.lock_and_select_all(&format!("SELECT music_id FROM songs WHERE music_id IN ({})", list), params!()).ok()?;
|
||||||
|
if alive.is_empty() {
|
||||||
|
let total: i64 = DATABASE.lock_and_select_type("SELECT COUNT(*) FROM songs", params!()).ok()?;
|
||||||
|
if total == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut rv = array![];
|
let mut rv = array![];
|
||||||
for id in ids {
|
for id in ids {
|
||||||
if !alive.contains(id) {
|
if !alive.contains(id) {
|
||||||
rv.push(id).unwrap();
|
rv.push(id).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rv
|
Some(rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every stored catalog blob, unparsed and unfiltered by visibility. Only the
|
// Every stored catalog blob, unparsed and unfiltered by visibility. Only the
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ pub struct Args {
|
|||||||
#[arg(long, default_value = "", help = "Asset hash for windows client.")]
|
#[arg(long, default_value = "", help = "Asset hash for windows client.")]
|
||||||
pub windows_asset_hash: String,
|
pub windows_asset_hash: String,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "", help = "Asset hash for linux client.")]
|
||||||
|
pub linux_asset_hash: String,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "", help = "Asset hash for macOS client.")]
|
||||||
|
pub mac_asset_hash: String,
|
||||||
|
|
||||||
#[arg(long, default_value = "", help = "Path to image assets.")]
|
#[arg(long, default_value = "", help = "Path to image assets.")]
|
||||||
pub image_asset_path: String,
|
pub image_asset_path: String,
|
||||||
|
|
||||||
|
|||||||
@@ -660,6 +660,9 @@ pub fn purge_song(music_id: i64) {
|
|||||||
// exists (browse renders an uploader name for every row). Same steps as the
|
// exists (browse renders an uploader name for every row). Same steps as the
|
||||||
// owner's own delete, blob GC included
|
// owner's own delete, blob GC included
|
||||||
pub fn purge_owner(uid: i64) {
|
pub fn purge_owner(uid: i64) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if disabled() {
|
if disabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -715,6 +718,12 @@ pub fn sweep_blobs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nothing referenced means the catalog is empty (or gone): a sweep would then delete
|
||||||
|
// every blob on disk, so it is skipped until the catalog has rows again
|
||||||
|
if referenced.is_empty() {
|
||||||
|
println!("Custom 3DMV blob sweep: catalog empty, skipped");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// No directory means nothing was ever uploaded
|
// No directory means nothing was ever uploaded
|
||||||
let Ok(entries) = fs::read_dir(get_data_path("custom_3dmv/blobs")) else {
|
let Ok(entries) = fs::read_dir(get_data_path("custom_3dmv/blobs")) else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1545,6 +1545,9 @@ pub fn delete_character(uid: i64, master_character_id: i64) -> Result<(), String
|
|||||||
// serving a dangling master_character_id, which the client throws on. Those stay,
|
// serving a dangling master_character_id, which the client throws on. Those stay,
|
||||||
// ownerless, which is the same trade delete_character already makes
|
// ownerless, which is the same trade delete_character already makes
|
||||||
pub fn purge_owner(uid: i64) {
|
pub fn purge_owner(uid: i64) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if disabled() {
|
if disabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,12 @@ pub fn sweep_audio() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nothing referenced means the catalog is empty (or gone): a sweep would then delete
|
||||||
|
// every ogg on disk, so it is skipped until the catalog has rows again
|
||||||
|
if referenced.is_empty() {
|
||||||
|
println!("Custom song audio sweep: catalog empty, skipped");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// No directory means nothing was ever uploaded
|
// No directory means nothing was ever uploaded
|
||||||
let Ok(entries) = fs::read_dir(get_data_path("custom_songs/audio")) else {
|
let Ok(entries) = fs::read_dir(get_data_path("custom_songs/audio")) else {
|
||||||
return;
|
return;
|
||||||
@@ -1334,6 +1340,9 @@ async fn delete(req: HttpRequest, body: String) -> HttpResponse {
|
|||||||
// the owner's own delete does, including the cross-feature MV cascade and the
|
// the owner's own delete does, including the cross-feature MV cascade and the
|
||||||
// content-addressed audio GC
|
// content-addressed audio GC
|
||||||
pub fn purge_owner(uid: i64) {
|
pub fn purge_owner(uid: i64) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if disabled() {
|
if disabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2724,6 +2733,123 @@ mod tests {
|
|||||||
assert!(crate::database::custom_card::get_character(character_id).is_none(), "the character survived the purge");
|
assert!(crate::database::custom_card::get_character(character_id).is_none(), "the character survived the purge");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A purge is scoped to ONE owner: nothing another account uploaded, nor an
|
||||||
|
// ogg two accounts share, may go with it - and a degenerate uid, a broken
|
||||||
|
// catalog or a disabled feature is a no-op, never a wildcard
|
||||||
|
#[test]
|
||||||
|
fn purging_one_account_leaves_other_accounts_uploads_alone() {
|
||||||
|
let _lock = crate::runtime::lock_test_data_path();
|
||||||
|
use crate::database::custom_card as card_db;
|
||||||
|
use crate::database::custom_3dmv as mv_db;
|
||||||
|
|
||||||
|
let victim = userdata::get_acc("custom-content-purge-victim")["user"]["id"].as_i64().unwrap();
|
||||||
|
let bystander = userdata::get_acc("custom-content-purge-bystander")["user"]["id"].as_i64().unwrap();
|
||||||
|
assert_ne!(victim, bystander);
|
||||||
|
for uid in [victim, bystander] {
|
||||||
|
purge_owner(uid);
|
||||||
|
crate::router::custom_card::purge_owner(uid);
|
||||||
|
crate::router::custom_3dmv::purge_owner(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One song each, plus one song each with byte-identical audio so the two
|
||||||
|
// owners share a single content-addressed ogg
|
||||||
|
let v_song = create_song(victim, &song_fields("Purge Victim", 401.0)).unwrap();
|
||||||
|
let b_song = create_song(bystander, &song_fields("Purge Bystander", 409.0)).unwrap();
|
||||||
|
let v_shared = create_song(victim, &song_fields("Purge Victim Shared", 419.0)).unwrap();
|
||||||
|
let b_shared = create_song(bystander, &song_fields("Purge Bystander Shared", 419.0)).unwrap();
|
||||||
|
let b_play = database::get_song(b_song).unwrap()["sound"]["play"]["md5"].to_string();
|
||||||
|
let shared_play = database::get_song(b_shared).unwrap()["sound"]["play"]["md5"].to_string();
|
||||||
|
assert_eq!(database::get_song(v_shared).unwrap()["sound"]["play"]["md5"].to_string(), shared_play);
|
||||||
|
|
||||||
|
// Cards and characters criss-crossed between the two owners
|
||||||
|
let b_char = card_db::next_character_id();
|
||||||
|
card_db::insert_character(b_char, bystander, &object!{ "master_character_id": b_char, "name": "B" }).unwrap();
|
||||||
|
let b_card = card_db::next_card_id();
|
||||||
|
card_db::insert_card(b_card, b_char, bystander, &object!{ "master_card_id": b_card, "rarity": 1 }, true, true).unwrap();
|
||||||
|
let v_card_on_b_char = card_db::next_card_id();
|
||||||
|
card_db::insert_card(v_card_on_b_char, b_char, victim, &object!{ "master_card_id": v_card_on_b_char, "rarity": 1 }, true, true).unwrap();
|
||||||
|
let v_char = card_db::next_character_id();
|
||||||
|
card_db::insert_character(v_char, victim, &object!{ "master_character_id": v_char, "name": "V" }).unwrap();
|
||||||
|
let b_card_on_v_char = card_db::next_card_id();
|
||||||
|
card_db::insert_card(b_card_on_v_char, v_char, bystander, &object!{ "master_card_id": b_card_on_v_char, "rarity": 1 }, true, true).unwrap();
|
||||||
|
let b_mv = mv_db::next_mv_id();
|
||||||
|
mv_db::insert_mv(b_mv, b_song, bystander, &object!{ "mv_id": b_mv, "music_id": b_song, "files": [] }, true).unwrap();
|
||||||
|
|
||||||
|
userdata::delete_account(victim);
|
||||||
|
|
||||||
|
assert!(database::get_song(v_song).is_none(), "the victim's song survived");
|
||||||
|
assert!(database::get_song(v_shared).is_none(), "the victim's shared-audio song survived");
|
||||||
|
assert!(card_db::get_card(v_card_on_b_char).is_none(), "the victim's card survived");
|
||||||
|
|
||||||
|
assert!(database::get_song(b_song).is_some(), "the bystander's song was deleted");
|
||||||
|
assert!(database::get_song(b_shared).is_some(), "the bystander's shared-audio song was deleted");
|
||||||
|
assert!(fs::metadata(get_data_path(&format!("custom_songs/{}", b_song))).is_ok(), "the bystander's song directory was deleted");
|
||||||
|
assert!(fs::read(audio_file_path(&b_play)).is_ok(), "the bystander's ogg was unlinked");
|
||||||
|
assert!(fs::read(audio_file_path(&shared_play)).is_ok(), "the cross-owner shared ogg was unlinked");
|
||||||
|
assert!(card_db::get_card(b_card).is_some(), "the bystander's card was deleted");
|
||||||
|
assert!(card_db::get_character(b_char).is_some(), "the bystander's character was deleted");
|
||||||
|
assert!(card_db::get_card(b_card_on_v_char).is_some(), "the bystander's card on the victim's character was deleted");
|
||||||
|
assert!(card_db::get_character(v_char).is_some(), "a character another account's card still uses was deleted");
|
||||||
|
assert!(mv_db::get_mv(b_mv).is_some(), "the bystander's MV was deleted");
|
||||||
|
|
||||||
|
purge_owner(0);
|
||||||
|
crate::router::custom_card::purge_owner(0);
|
||||||
|
crate::router::custom_3dmv::purge_owner(0);
|
||||||
|
purge_owner(-1);
|
||||||
|
userdata::delete_account(0);
|
||||||
|
with_songs_table_broken(|| purge_owner(bystander));
|
||||||
|
crate::runtime::set_enable_custom_songs(false);
|
||||||
|
crate::runtime::set_enable_custom_cards(false);
|
||||||
|
crate::runtime::set_enable_custom_3dmv(false);
|
||||||
|
purge_owner(bystander);
|
||||||
|
crate::router::custom_card::purge_owner(bystander);
|
||||||
|
crate::router::custom_3dmv::purge_owner(bystander);
|
||||||
|
crate::runtime::set_enable_custom_songs(true);
|
||||||
|
crate::runtime::set_enable_custom_cards(true);
|
||||||
|
crate::runtime::set_enable_custom_3dmv(true);
|
||||||
|
assert!(database::get_song(b_song).is_some(), "a degenerate, broken-catalog or feature-off purge deleted a song");
|
||||||
|
assert!(fs::read(audio_file_path(&b_play)).is_ok(), "a degenerate, broken-catalog or feature-off purge unlinked a live ogg");
|
||||||
|
assert!(card_db::get_card(b_card).is_some(), "a degenerate or feature-off purge deleted a card");
|
||||||
|
assert!(card_db::get_character(b_char).is_some(), "a degenerate or feature-off purge deleted a character");
|
||||||
|
assert!(mv_db::get_mv(b_mv).is_some(), "a degenerate or feature-off purge deleted an MV");
|
||||||
|
|
||||||
|
// Leave nothing behind: other tests assume an empty obtainable-card pool
|
||||||
|
purge_owner(bystander);
|
||||||
|
crate::router::custom_card::purge_owner(bystander);
|
||||||
|
crate::router::custom_3dmv::purge_owner(bystander);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pruning get_acc runs on every login must read an unreadable or blank
|
||||||
|
// catalog as "unknown", never as "every custom song is dead", and the startup
|
||||||
|
// sweep must never unlink files when the catalog references nothing
|
||||||
|
#[test]
|
||||||
|
fn an_unreadable_or_blank_catalog_never_prunes_or_sweeps() {
|
||||||
|
let _lock = crate::runtime::lock_test_data_path();
|
||||||
|
let uid = userdata::get_acc("custom-content-blank-catalog")["user"]["id"].as_i64().unwrap();
|
||||||
|
purge_owner(uid);
|
||||||
|
let music_id = create_song(uid, &song_fields("Blank Catalog", 383.0)).unwrap();
|
||||||
|
let play = database::get_song(music_id).unwrap()["sound"]["play"]["md5"].to_string();
|
||||||
|
let live_ogg = audio_file_path(&play);
|
||||||
|
|
||||||
|
let unreadable = with_songs_table_broken(|| database::dead_music_ids(&array![music_id]));
|
||||||
|
assert!(unreadable.is_none(), "an unreadable catalog reported dead songs");
|
||||||
|
with_songs_table_broken(|| sweep_audio());
|
||||||
|
assert!(fs::read(&live_ogg).is_ok(), "a sweep over an unreadable catalog unlinked a live ogg");
|
||||||
|
|
||||||
|
let conn = rusqlite::Connection::open(database::test_db_path()).unwrap();
|
||||||
|
conn.execute("ALTER TABLE songs RENAME TO songs_parked", ()).unwrap();
|
||||||
|
conn.execute("CREATE TABLE songs (music_id BIGINT NOT NULL PRIMARY KEY, owner_id BIGINT NOT NULL, song TEXT NOT NULL, visibility TEXT NOT NULL DEFAULT 'public', downloads_disabled INT NOT NULL DEFAULT 0)", ()).unwrap();
|
||||||
|
let blank = database::dead_music_ids(&array![music_id]);
|
||||||
|
sweep_audio();
|
||||||
|
let survived = fs::read(&live_ogg).is_ok();
|
||||||
|
conn.execute("DROP TABLE songs", ()).unwrap();
|
||||||
|
conn.execute("ALTER TABLE songs_parked RENAME TO songs", ()).unwrap();
|
||||||
|
assert!(blank.is_none(), "a blank catalog reported every song dead");
|
||||||
|
assert!(survived, "a sweep over a blank catalog unlinked a live ogg");
|
||||||
|
assert_eq!(database::dead_music_ids(&array![music_id]).unwrap().len(), 0);
|
||||||
|
purge_owner(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// D1/D6/D7: the whole upload route, end to end - a real multipart body, a real
|
// D1/D6/D7: the whole upload route, end to end - a real multipart body, a real
|
||||||
// session, the expansion of a real package, and the create running on the
|
// session, the expansion of a real package, and the create running on the
|
||||||
|
|||||||
+34
-2
@@ -49,6 +49,10 @@ static ASSET_VERSIONS: &[AssetVersion] = &[
|
|||||||
AssetVersion { region: "JP", platform: "Windows", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "b8d0e7edcb63f5bdd28817a597772ca6", latest: true },
|
AssetVersion { region: "JP", platform: "Windows", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "b8d0e7edcb63f5bdd28817a597772ca6", latest: true },
|
||||||
AssetVersion { region: "JP", platform: "Android", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "d12cecc5695da7f81f8873a3ff93752e", latest: true },
|
AssetVersion { region: "JP", platform: "Android", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "d12cecc5695da7f81f8873a3ff93752e", latest: true },
|
||||||
AssetVersion { region: "JP", platform: "iOS", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "7c1f61ee68ac84c82dd397a162629142", latest: true },
|
AssetVersion { region: "JP", platform: "iOS", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "7c1f61ee68ac84c82dd397a162629142", latest: true },
|
||||||
|
// Linux / macOS players (StandaloneLinux64 / StandaloneOSX lanes): no baked hash, the
|
||||||
|
// running build's hash is supplied with --linux-asset-hash / --mac-asset-hash
|
||||||
|
AssetVersion { region: "JP", platform: "Linux", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "", latest: true },
|
||||||
|
AssetVersion { region: "JP", platform: "Mac", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "", latest: true },
|
||||||
|
|
||||||
//AssetVersion { region: "JP", platform: "WebGL", version: "4c921d2443335e574a82e04ec9ea243c", hash: "e1ff7c74b20c8d216507972b6f24b9df", latest: true },
|
//AssetVersion { region: "JP", platform: "WebGL", version: "4c921d2443335e574a82e04ec9ea243c", hash: "e1ff7c74b20c8d216507972b6f24b9df", latest: true },
|
||||||
];
|
];
|
||||||
@@ -68,6 +72,8 @@ impl AssetVersion {
|
|||||||
let ov = args.asset_version.as_str();
|
let ov = args.asset_version.as_str();
|
||||||
let oh = match (self.region, self.platform) {
|
let oh = match (self.region, self.platform) {
|
||||||
("JP", "Windows") => args.windows_asset_hash.as_str(),
|
("JP", "Windows") => args.windows_asset_hash.as_str(),
|
||||||
|
("JP", "Linux") => args.linux_asset_hash.as_str(),
|
||||||
|
("JP", "Mac") => args.mac_asset_hash.as_str(),
|
||||||
("JP", "Android") => args.jp_android_asset_hash.as_str(),
|
("JP", "Android") => args.jp_android_asset_hash.as_str(),
|
||||||
("JP", "iOS") => args.jp_ios_asset_hash.as_str(),
|
("JP", "iOS") => args.jp_ios_asset_hash.as_str(),
|
||||||
("GL", "Android") => args.en_android_asset_hash.as_str(),
|
("GL", "Android") => args.en_android_asset_hash.as_str(),
|
||||||
@@ -89,7 +95,7 @@ fn valid_hashes(asset_version: &str, platform: &str) -> Vec<String> {
|
|||||||
if entry.platform != platform {
|
if entry.platform != platform {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if entry.version == asset_version {
|
if entry.version == asset_version && !entry.hash.is_empty() {
|
||||||
out.push(entry.stock_hash());
|
out.push(entry.stock_hash());
|
||||||
}
|
}
|
||||||
if entry.latest {
|
if entry.latest {
|
||||||
@@ -116,7 +122,7 @@ fn preferred_hash(asset_version: &str, platform: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if entry.version == asset_version && stock.is_none() {
|
if entry.version == asset_version && stock.is_none() && !entry.hash.is_empty() {
|
||||||
stock = Some(entry.stock_hash());
|
stock = Some(entry.stock_hash());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +175,8 @@ pub fn parse_platform(header: &str) -> &str {
|
|||||||
"iphone" => "iOS",
|
"iphone" => "iOS",
|
||||||
"windows" => "Windows",
|
"windows" => "Windows",
|
||||||
"windowsplayer" => "Windows",
|
"windowsplayer" => "Windows",
|
||||||
|
"linuxplayer" => "Linux",
|
||||||
|
"osxplayer" => "Mac",
|
||||||
"webglplayer" => "WebGL",
|
"webglplayer" => "WebGL",
|
||||||
"editor" => "Editor",
|
"editor" => "Editor",
|
||||||
"windowseditor" => "Editor",
|
"windowseditor" => "Editor",
|
||||||
@@ -471,3 +479,27 @@ pub(crate) fn get_cards(arr: JsonValue, user: &JsonValue) -> JsonValue {
|
|||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod platform_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standalone_player_platforms_are_known() {
|
||||||
|
assert_eq!(parse_platform("WindowsPlayer"), "Windows");
|
||||||
|
assert_eq!(parse_platform("LinuxPlayer"), "Linux");
|
||||||
|
assert_eq!(parse_platform("OSXPlayer"), "Mac");
|
||||||
|
assert_eq!(parse_platform("OSXEditor"), "Editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux / macOS have no baked hash: without the CLI override the platform has no valid
|
||||||
|
// hash at all (never an empty string presented as one)
|
||||||
|
#[test]
|
||||||
|
fn unhashed_platforms_answer_nothing_without_an_override() {
|
||||||
|
let latest = ASSET_VERSIONS.iter().find(|e| e.latest && e.platform == "Windows").unwrap().version;
|
||||||
|
assert_eq!(preferred_hash(latest, "Mac"), None);
|
||||||
|
assert_eq!(preferred_hash(latest, "Linux"), None);
|
||||||
|
assert!(valid_hashes(latest, "Mac").is_empty());
|
||||||
|
assert!(preferred_hash(latest, "Windows").is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ fn remove_deleted_custom_songs(user: &mut JsonValue) -> bool {
|
|||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let dead = custom_song::dead_music_ids(&candidates);
|
let Some(dead) = custom_song::dead_music_ids(&candidates) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
if dead.is_empty() {
|
if dead.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -301,7 +303,9 @@ fn remove_deleted_custom_cards(user: &mut JsonValue) -> bool {
|
|||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let dead = custom_card::dead_card_ids(&candidates);
|
let Some(dead) = custom_card::dead_card_ids(&candidates) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
if dead.is_empty() {
|
if dead.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fn platform_guard(ctx: &guard::GuardContext) -> bool {
|
|||||||
.split('/')
|
.split('/')
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
matches!(platform, "Android" | "StandaloneWindows64" | "StandaloneLinux64" | "WebGL" | "iOS")
|
matches!(platform, "Android" | "StandaloneWindows64" | "StandaloneLinux64" | "StandaloneOSX" | "WebGL" | "iOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes(cfg: &mut web::ServiceConfig) {
|
pub fn routes(cfg: &mut web::ServiceConfig) {
|
||||||
|
|||||||
Reference in New Issue
Block a user