Skip to content

Commit c5377f8

Browse files
committed
enh(app/skin-selector): better DB intension through deferred FKs, further PNG validations
1 parent c0b0946 commit c5377f8

File tree

4 files changed

+63
-26
lines changed

4 files changed

+63
-26
lines changed

packages/app-lib/migrations/20250413162050_skin-selector.sql

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ CREATE TABLE custom_minecraft_skins (
1515

1616
PRIMARY KEY (minecraft_user_uuid, texture_key, variant, cape_id),
1717
FOREIGN KEY (minecraft_user_uuid) REFERENCES minecraft_users(uuid)
18-
ON DELETE CASCADE ON UPDATE CASCADE
18+
ON DELETE CASCADE ON UPDATE CASCADE,
19+
FOREIGN KEY (texture_key) REFERENCES custom_minecraft_skin_textures(texture_key)
20+
ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED
1921
);
2022

2123
CREATE TABLE custom_minecraft_skin_textures (
@@ -25,26 +27,10 @@ CREATE TABLE custom_minecraft_skin_textures (
2527
PRIMARY KEY (texture_key)
2628
);
2729

28-
-- Use triggers to emulate partial cascading foreign key constraints on the custom_minecraft_skin_textures table
29-
30-
CREATE TRIGGER custom_minecraft_skin_texture_insertion_validation
31-
BEFORE INSERT ON custom_minecraft_skin_textures FOR EACH ROW
32-
BEGIN
33-
SELECT CASE WHEN NOT EXISTS (
34-
SELECT 1 FROM custom_minecraft_skins WHERE texture_key = NEW.texture_key
35-
) THEN RAISE(ABORT, 'Missing custom skin for the specified skin texture key') END;
36-
END;
37-
3830
CREATE TRIGGER custom_minecraft_skin_texture_delete_cleanup
3931
AFTER DELETE ON custom_minecraft_skins FOR EACH ROW
4032
BEGIN
4133
DELETE FROM custom_minecraft_skin_textures WHERE texture_key NOT IN (
4234
SELECT texture_key FROM custom_minecraft_skins
4335
);
4436
END;
45-
46-
CREATE TRIGGER custom_minecraft_skin_texture_update_cleanup
47-
AFTER UPDATE OF texture_key ON custom_minecraft_skins FOR EACH ROW
48-
BEGIN
49-
UPDATE custom_minecraft_skin_textures SET texture_key = NEW.texture_key WHERE texture_key = OLD.texture_key;
50-
END;

packages/app-lib/src/api/minecraft_skins.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ pub async fn get_available_skins() -> crate::Result<Vec<Skin>> {
185185
// Several custom skins may reuse the same texture for different cape or skin model
186186
// variations, so check all attributes for correctness
187187
let is_equipped = !found_equipped_skin.load(Ordering::Acquire)
188-
&& custom_skin.texture_key == &*current_skin.texture_key()
188+
&& custom_skin.texture_key == *current_skin.texture_key()
189189
&& custom_skin.variant == current_skin.variant
190190
&& custom_skin.cape_id
191191
== if custom_skin.cape_id.is_some() {
@@ -261,6 +261,11 @@ pub async fn add_and_equip_custom_skin(
261261
variant: MinecraftSkinVariant,
262262
cape_override: Option<Cape>,
263263
) -> crate::Result<()> {
264+
let (skin_width, skin_height) = png_dimensions(&texture_blob)?;
265+
if skin_width != 64 || ![32, 64].contains(&skin_height) {
266+
return Err(ErrorKind::InvalidSkinTexture)?;
267+
}
268+
264269
let cape_override = cape_override.map(|cape| cape.id);
265270
let state = State::get().await?;
266271

@@ -270,7 +275,7 @@ pub async fn add_and_equip_custom_skin(
270275

271276
// We have to equip the skin first, as it's the Mojang API backend who knows
272277
// how to compute the texture key we require, which we can then read from the
273-
// updated player profile. This also ensures the skin data is indeed valid
278+
// updated player profile
274279
mojang_api::MinecraftSkinOperation::equip(
275280
&selected_credentials,
276281
stream::iter([Ok::<_, String>(Bytes::clone(&texture_blob))]),
@@ -478,13 +483,15 @@ async fn sync_cape(
478483
Ok(())
479484
}
480485

481-
fn texture_blob_to_data_url(texture_blob: Option<Vec<u8>>) -> Arc<Url> {
482-
let data = texture_blob.map_or(
486+
fn texture_blob_to_data_url(texture_blob: Vec<u8>) -> Arc<Url> {
487+
let data = if is_png(&texture_blob) {
488+
Cow::Owned(texture_blob)
489+
} else {
490+
// Fall back to a placeholder texture if the DB somehow contains corrupt data
483491
Cow::Borrowed(
484492
&include_bytes!("minecraft_skins/assets/default/MissingNo.png")[..],
485-
),
486-
Cow::Owned,
487-
);
493+
)
494+
};
488495

489496
Url::parse(&format!(
490497
"data:image/png;base64,{}",
@@ -493,3 +500,39 @@ fn texture_blob_to_data_url(texture_blob: Option<Vec<u8>>) -> Arc<Url> {
493500
.unwrap()
494501
.into()
495502
}
503+
504+
fn is_png(data: &[u8]) -> bool {
505+
/// The initial 8 bytes of a PNG file, used to identify it as such.
506+
///
507+
/// Reference: <https://www.w3.org/TR/png-3/#3PNGsignature>
508+
const PNG_SIGNATURE: &[u8] =
509+
&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
510+
511+
data.starts_with(PNG_SIGNATURE)
512+
}
513+
514+
fn png_dimensions(data: &[u8]) -> crate::Result<(u32, u32)> {
515+
if !is_png(data) {
516+
Err(ErrorKind::InvalidPng)?;
517+
}
518+
519+
// Read the width and height fields from the IHDR chunk, which the
520+
// PNG specification mandates to be the first in the file, just after
521+
// the 8 signature bytes. See:
522+
// https://www.w3.org/TR/png-3/#5DataRep
523+
// https://www.w3.org/TR/png-3/#11IHDR
524+
let width = u32::from_be_bytes(
525+
data.get(16..20)
526+
.ok_or(ErrorKind::InvalidPng)?
527+
.try_into()
528+
.unwrap(),
529+
);
530+
let height = u32::from_be_bytes(
531+
data.get(20..24)
532+
.ok_or(ErrorKind::InvalidPng)?
533+
.try_into()
534+
.unwrap(),
535+
);
536+
537+
Ok((width, height))
538+
}

packages/app-lib/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ pub enum ErrorKind {
128128

129129
#[error("Invalid data URL: {0}")]
130130
InvalidDataUrlBase64(#[from] data_url::forgiving_base64::InvalidBase64),
131+
132+
#[error("Invalid PNG")]
133+
InvalidPng,
134+
135+
#[error(
136+
"A skin texture must have a dimension of either 64x64 or 64x32 pixels"
137+
)]
138+
InvalidSkinTexture,
131139
}
132140

133141
#[derive(Debug)]

packages/app-lib/src/state/minecraft_skins/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,12 @@ impl CustomMinecraftSkin {
150150
pub async fn texture_blob(
151151
&self,
152152
db: impl sqlx::Acquire<'_, Database = sqlx::Sqlite>,
153-
) -> crate::Result<Option<Vec<u8>>> {
153+
) -> crate::Result<Vec<u8>> {
154154
Ok(sqlx::query_scalar!(
155155
"SELECT texture FROM custom_minecraft_skin_textures WHERE texture_key = ?",
156156
self.texture_key
157157
)
158-
.fetch_optional(&mut *db.acquire().await?)
158+
.fetch_one(&mut *db.acquire().await?)
159159
.await?)
160160
}
161161

0 commit comments

Comments
 (0)