From 6b08fec0fc79cd135232fd3a57b41a515bb544db Mon Sep 17 00:00:00 2001 From: Tony Frazier <16245649+a-frazier@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:46:59 -0500 Subject: [PATCH] Fix CRLF Migrations (#162) Fix migrations originally created on Windows systems with CRLF line endings --- .gitattributes | 6 +++ lefthook.yml | 6 +++ scripts/check_migrations.js | 27 +++++++++++++ src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 2 + src-tauri/migrations/.editorconfig | 5 +++ src-tauri/src/lib.rs | 18 ++++++++- src-tauri/src/migration_patch.rs | 63 ++++++++++++++++++++++++++++++ 8 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 scripts/check_migrations.js create mode 100644 src-tauri/migrations/.editorconfig create mode 100644 src-tauri/src/migration_patch.rs diff --git a/.gitattributes b/.gitattributes index 8dc92787..fb8eaca2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,8 @@ # Force LF line endings for all text files * text=auto eol=lf + +# Migrations SQL in particular must have uniform line endings in all scenarios. +# SQLx relies on consistent checksums (SHA384 hashes at the time of writing) +# to verify that migrations have not changed. This ensures that line endings +# stay consistent between platforms and systems regardless of autocrlf setup. +src-tauri/migrations/*.sql text eol=lf diff --git a/lefthook.yml b/lefthook.yml index bede5bf8..4c5c1324 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,3 +1,9 @@ +pre-commit: + commands: + check_migrations: + glob: "src-tauri/migrations/*.sql" + run: node scripts/check_migrations.js {staged_files} + pre-push: commands: lint: diff --git a/scripts/check_migrations.js b/scripts/check_migrations.js new file mode 100644 index 00000000..ab136fc8 --- /dev/null +++ b/scripts/check_migrations.js @@ -0,0 +1,27 @@ +import { readdirSync, readFileSync } from 'fs' +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const migrationsDir = resolve(__dirname, '../src-tauri/migrations') + +const files = + process.argv.length > 2 + ? process.argv.slice(2) + : readdirSync(migrationsDir) + .filter((f) => f.endsWith('.sql')) + .map((f) => resolve(migrationsDir, f)) + +let failed = false + +for (const file of files) { + const content = readFileSync(file, 'utf8') + if (content.includes('\r')) { + console.error(`CRLF line endings detected: ${file}`) + failed = true + } +} + +if (failed) { + process.exit(1) +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ced303a8..230a5424 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -286,12 +286,14 @@ version = "0.7.0-pre.3" dependencies = [ "axum 0.8.8", "base64 0.22.1", + "hex", "image", "local-ip-address 0.6.8", "qrcode", "reqwest", "serde", "serde_json", + "sqlx", "tauri", "tauri-build", "tauri-plugin-devtools", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6b2b8ecc..b97208ec 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -38,3 +38,5 @@ local-ip-address = "0.6" uuid = { version = "1", features = ["v4"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } tauri-plugin-devtools = { version = "2", optional = true } +sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio"] } +hex = "0.4.3" diff --git a/src-tauri/migrations/.editorconfig b/src-tauri/migrations/.editorconfig new file mode 100644 index 00000000..5fd2d3fe --- /dev/null +++ b/src-tauri/migrations/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.sql] +charset = utf-8 +end_of_line = lf diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 86e276f9..47ebe617 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,7 @@ +use tauri::Manager; use tauri_plugin_sql::{Migration, MigrationKind}; +mod migration_patch; mod sync; use sync::commands::{ @@ -206,7 +208,8 @@ pub fn run() { let mut builder = tauri::Builder::default(); - #[cfg(all(debug_assertions, feature = "devtools"))] // only enable instrumentation in development builds + #[cfg(all(debug_assertions, feature = "devtools"))] + // only enable instrumentation in development builds { builder = builder.plugin(tauri_plugin_devtools::init()); } @@ -219,6 +222,19 @@ pub fn run() { .add_migrations("sqlite:aventura.db", migrations) .build(), ) + .setup(|app| { + let db_path = app + .path() + .app_data_dir() + .expect("failed to get app data dir") + .join("aventura.db"); + + if db_path.try_exists().expect("failed to check db path") { + tauri::async_runtime::block_on(migration_patch::apply_checksum_patch(&db_path)); + } + + Ok(()) + }) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_updater::Builder::new().build()) diff --git a/src-tauri/src/migration_patch.rs b/src-tauri/src/migration_patch.rs new file mode 100644 index 00000000..454c0b26 --- /dev/null +++ b/src-tauri/src/migration_patch.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +pub async fn apply_checksum_patch(db_path: &PathBuf) { + // Tuple structure + // * 1st field - Migration version + // * 2nd field - Bad CRLF Checksum + // * 3rd field - Good LF Checksum + let fixups: &[(i64, &str, &str)] = &[ + (1, "80961C08C9AB9078DE7962994CB834182FE654B8A556CC4007616AC84761F9A672169CF53B74E67B49BBFA0A03DBF992", "45DA77529E456E527B54A698A60CEB116918F72B753A751B2646929268787C56EC0552EBCE30B8501D11210A56413EEE"), + (2, "1E5590E528E0633027A04887DAB71B7A94B8CCDE46396318FA8BC5CF50E91EF9EF50829D9625DB5C18A9A4E783B9C724", "D5FC3CF234782E9FB48C4E294848301588E0A180217EECD74314353C01F88383A3A0DCD3583E0BA19C79F11C190BBE5F"), + (3, "506D827203B42F9A3D899E8442C8FD37F9CADC740F1BA86F8AB1188397DF7E9165E4B31DE8670E81242E42967B4D5F4C", "3216C4EFC2E016C2A8952B78F472BE07029537C077CE8967AD8AEADCB11FF57ECD47EF192C9D5CAE6F7F110F245D78C5"), + (4, "F3783F2D6F73289783A660AAFDADD62FBEC7AF1710124376E5669978309824EF4641AF20F0C75F50C1F3F59B51779A8F", "FBE05AE8F7E494EFD817CFD6C8557CB2E65F13A8471814765B287234B719A3CC05358C82BCEEB8341BA77BCDA5D88F7E"), + (5, "67918F657AC44D833256697B2CB1B48789CA997C84FAA07C3682BB3722D4CE8434B7857F53C84BA18D9258FB1FDFF9F8", "25BE4F4C3C52CF996A2CA68B8C9D103A49922D7CACA45BC886895C3293DB3370837A54AB45DD59D0B2C840150417A9AC"), + (6, "56DB36F22D11426A18725763292B9968438D5CF978D518EE70D057ED7FDA6173F3191AA3847E9A0D5BF6793D69933345", "66901605CF15AAF8C393B5BA399341F35799C4130C234D7F82C0FC5EEE25F2FF0C1BD6124E16BDBD7DACD566DAB3E838"), + (7, "E4033780CD513A8F812E5D8E115D49E7C3535D51907AE1E2F1B8D10864FFA2176EA475F417CC93E8382D7DFC7B9A45F8", "CA02DF9CC1C9DE198162278148B0D5606151D1567FF778FF4C709F420100EE94FA2F753DDC0DB5B4B0147C64E3F06F5E"), + (8, "68606B555B1418C60C68DFEF64F1B523721528241636948E11EE98A58E1F72531F6A182D5BF2C2EE1B7B69922F019E74", "7914E875C0C87FA5B695A940CA64A4CCE9E26989ADD2F6D6AA724434D48E8AB88FDC111448E4C9D6BF96EB13A04A13A0"), + (9, "67C62F366C4C4D6A2A39879FE865B72FB806C111FD887E6DC977A537C5B5C8A1F31F8237CA1EFB2E41B063E4E0DFE953", "D474DF1322AA9FC5ADBF8011F8F5972DF969E5E2F1C2DDE617390BB2ECD506D927B8D5A4D246E6AC01C9DD294D3F59DA"), + (10, "B7D81C8D0F477C99B06D6ECB190CFE579E3E5D28E0006F1AB91D8ACB6A625DFD3509BDD8286A7382C219CD7175D89EE8", "EE1C4B7D3AA69D108F604E79C6B7FB739111F76BFB5508D84EAFEED19A00427756CDAB6B0E9D44122BB6707E77FF4448"), + (11, "9ECBC1666EC858C1C57A393B968FF1AF7EDC0AC31B9ADAB452592F14A2BF8E82E47773C1B046F4464553A01609EBE6B9", "57A1FA86177C63D3C166B49EAB859AF2ED537D16D860B420055AE82969FC7A7AA6BE4B4D1F63E37F5FAAF432B97733B3"), + (12, "5A96C0FAFBA9C232F56524B92F77C9781D0F7A188DD29355FBFDBBB6FCE99B4A3744BF6D5016DC59C693ED43C0F0B605", "800194075E7E977E865B34D9C0B24225429A810582A4E0BD5A991C3B18C208ACC9EA1F9B23A6557C019CA8471AF78538"), + (13, "2EFFFB25AC647EBA3639B76E295F4BAC0C131E4320434C233C046C7303A10C1BA0588B58CAC2ADF521ED22114FA33D94", "78CC8D573EC459D9C1F4ECCCC8C570BB1F5A2EB70B6E53C8792B3B4ED05C9E49FD400DDFE229C0F76AC886BE4EBF625B"), + (14, "E702F3FED6D91843C0F9D783389F3DE6E71763795D23E9D78195D7C05188E4630CC2DBE4F009A8ABCDE11B5BDE52CE45", "C0E40E74674C907D0408B1ED2B6807451C308FCBA2342C3AC8BC74DBADD1D8C855B0E8129871EE69DAF69FCB622E2C50"), + (15, "3ADCE4ACFABC707AACA1CCB7715AF977B9AC6AF88E4D14EC7A83E3DD50083189F7B1F2363815F50221F9CC2CAFD78C58", "C4FA27EA2A5EDFFC5C6409DAE12D17929B7D138D983F0186AC73A978810FE07909F814351A2B1E4BC356D14E2F833E6F"), + (16, "6C5BB0966CEEE341971B618C6CD773AC5074B2FD5FD2EE4B9050000319BF63EAE75569D724AC470EEE2DF9EEE93FFCFE", "A1D4EE745DD7579C64405319765250CE4F240CDB7844570FEB833EFD1EE9167E5812DEB06751B176DF8F3FEE1623B4AC"), + (17, "5AC750445870E7A521EAC7C923797F7F63F7276FFAD8D82C2A1706E01D5830B54DB2DF09B91D80487DFC4CD3AFA8D443", "F3FE334A6E8C1EE2BFEC76EA7EE6864F9B3955D6FFE2457C1C3C7779D1C1EC3623FDA7793E5A909DFE5E17F23EDD85B2"), + (18, "E02046D1CB78683B584A1A2C1DE0545E2630D41EDCE50F63F733A29494CD24573AB4F4773538A4EFCFA0B875DBF24B1C", "C159B2590C93055B83D4277EEBC30D842BC6204189CACA54E289F65154B28E7D1215F9290D4D0CC490BDC64D7D44D926"), + (19, "1780E807250AD7465D349D1A1217897616D529F734BC620A71109B70E9148B57614C6223DDC282DFDD4DCA43AFCD02D5", "000B050F0EE6F0968682BDC52C6E2C1A1A48D9AC65806D9D4203088C74AFC3DA8853B77D4FE006D33358F6AACDDB7296"), + (20, "3A4D6765C2C95C262231BA2C49581481DABCAAAA1471FBDCFE5469AB87EFB9522CA3B9D21E1BF21B496C006F1063E6DB", "08347C1ACB35B2C40A64620F697EA5C6DAC31743B0C7918655EAF816CAFA905F07FAC7833BDBCE9594F937B39A7765D2"), + (21, "9AD364A98418C4CF042C80D5C22CE9E20550A409E7FC895084EFBC26D8060904BFEEF2FE9BF2DD9A1DCE2383BACABF3A", "EE146CE53919F3556BA55E90FF05A8226AF4CB177FF1F3AD97C4B7C2212DCFF4AEFC2B8E90BB96D51084B1D4AD34EC91"), + (22, "DEFB01539A9F501750F34D0C095696A3E3DE920558A5EF392B28CCF6192BDFCA3BFFA0AE73AAEEB9A31849711602DA62", "CA7858368F9A9302C497D114C466D5524D5C8B05275CE38E5F93945060B7A8D5BA5BFDA76085E4BA61A44D95B392F0D1"), + (23, "DEBD26CF8E08A5B2D164ED1A2FB1CA83283E5246299B64E1314DF9FEC8123E4DCC55E543037DE6539E511CE0C7D9CCEB", "C48FC82ED86CD6902FE8A3302D4C1F2967F5FDC1FBB9E9CF47834636F1733A1EA46C41AAC4C42C4CA16763336384A922"), + (24, "92696F83EE49FE36C522AC39428EB84B54D7DC246291592B1BB32F4A1E6F0835DD1BB9C24A90C74391FE893041F9C170", "11100B66FFF8AD121D91F7AFAE4FC96F1FB1DFA827146FAD68B95E72FBA13CA513A8E011F7E1763E72E6E8BBB6EFCC7D"), + ]; + + let pool = sqlx::SqlitePool::connect(&format!("sqlite:{}", db_path.display())) + .await + .expect("failed to open db for checksum fixup"); + + let total_migrations: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM _sqlx_migrations") + .fetch_one(&pool) + .await + .expect("failed to probe migrations"); + + if total_migrations <= fixups.len() as i64 { + let mut tx = pool.begin().await.expect("failed to begin transaction"); + + for (version, crlf_hex, lf_hex) in fixups { + let crlf_bytes = hex::decode(crlf_hex).expect("bad crlf hex"); + let lf_bytes = hex::decode(lf_hex).expect("bad lf hex"); + sqlx::query( + "UPDATE _sqlx_migrations SET checksum = $1 WHERE version = $2 AND checksum = $3", + ) + .bind(lf_bytes) + .bind(version) + .bind(crlf_bytes) + .execute(&mut *tx) + .await + .expect("checksum fixup failed"); + } + + tx.commit().await.expect("failed to commit checksum fixup"); + } +}