Skip to content

Commit 3924dea

Browse files
authored
Merge branch 'main' into e-api-again
2 parents c2a0f23 + 08bbe52 commit 3924dea

27 files changed

Lines changed: 1452 additions & 269 deletions

Refresh.Core/Extensions/GameDatabaseContextExtensions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
using Refresh.Database;
22
using Refresh.Database.Models.Assets;
3+
using Refresh.Database.Models.Authentication;
34
using Refresh.Database.Models.Levels;
45

56
namespace Refresh.Core.Extensions;
67

78
public static class GameDatabaseContextExtensions
89
{
9-
public static void UpdateLevelModdedStatus(this GameDatabaseContext database, GameLevel level)
10+
public static void UpdateLevelModdedStatus(this GameDatabaseContext database, GameLevel level, bool save = true)
1011
{
11-
database.SetLevelModdedStatus(level, database.GetLevelModdedStatus(level));
12+
database.SetLevelModdedStatus(level, database.GetLevelModdedStatus(level), save);
1213
}
1314

1415
public static bool GetLevelModdedStatus(this GameDatabaseContext database, GameLevel level)
1516
{
17+
// Skip this for PSP assets, as we can't read them nor determine their type (yet)
18+
if (level.GameVersion == TokenGame.LittleBigPlanetPSP)
19+
return false;
20+
1621
bool modded = false;
1722

1823
GameAsset? rootAsset = database.GetAssetFromHash(level.RootResource);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Diagnostics;
2+
using Bunkum.Core;
3+
using NotEnoughLogs;
4+
using Refresh.Common.Verification;
5+
using Refresh.Core.Types.Assets.Validation;
6+
using Refresh.Database.Models.Assets;
7+
using Refresh.Database.Models.Authentication;
8+
9+
namespace Refresh.Core.Helpers;
10+
11+
public abstract class ResourceValidationHelper
12+
{
13+
/// <summary>
14+
/// Validates the given asset reference (hash/guid/blank) using the given parameters, if necessary also by reading the referenced asset
15+
/// or getting data about it from database (see <see cref="GameAsset"/> and <see cref="DisallowedAsset"/>).
16+
/// </summary>
17+
public static ValidatedAssetResult ValidateReference(AssetValidationParameters parameters, Logger logger)
18+
{
19+
string assetTypeStr = parameters.AssetContextTypeStr ?? (parameters.MustBeTexture ? "image asset" : "asset");
20+
GameAsset? asset = null;
21+
bool existsInDataStore = false;
22+
bool isPSP = parameters.GameToUseIn == TokenGame.LittleBigPlanetPSP;
23+
Action<string>? onNewAssetRefCallback = parameters.OnNewAssetRefCallback;
24+
25+
if (parameters.AssetRef.IsBlankHash())
26+
{
27+
if (!parameters.MayBeBlank) return new(BadRequest, "0", $"The {assetTypeStr} must be set.", onNewAssetRefCallback);
28+
else return new(OK, "0", null, onNewAssetRefCallback);
29+
}
30+
31+
else if (parameters.AssetRef.StartsWith('g'))
32+
{
33+
if (!parameters.MayBeGuid) return new(BadRequest, null, $"The {assetTypeStr} may not be an in-game asset.", onNewAssetRefCallback);
34+
if (parameters.AssetRef.Length < 2) return new(BadRequest, null, $"The used in-game {assetTypeStr} is invalid (empty GUID).", onNewAssetRefCallback);
35+
36+
// This should only happen if the user is messing with mods/the API/beta builds, so give them a more detailed response
37+
bool canParseGuid = long.TryParse(parameters.AssetRef[1..], out long guid);
38+
if (!canParseGuid)
39+
return new(BadRequest, null, $"The used in-game {assetTypeStr} is invalid (badly formatted GUID).", onNewAssetRefCallback);
40+
41+
if (parameters.MustBeTexture && !parameters.GuidChecker.IsTextureGuid(parameters.GameToUseIn, guid))
42+
return new(BadRequest, null, $"The used in-game {assetTypeStr} was not a valid image (unknown GUID).", onNewAssetRefCallback);
43+
}
44+
45+
// At this point the reference is a hash
46+
else if (!parameters.MayBeHash)
47+
{
48+
return new(BadRequest, null, $"The {assetTypeStr} may not be a custom asset.", onNewAssetRefCallback);
49+
}
50+
51+
else if (!CommonPatterns.Sha1Regex().IsMatch(parameters.AssetRef))
52+
{
53+
// This should only happen if a player is messing with mods/the API, so give them a more detailed response
54+
return new(BadRequest, null, $"The used {assetTypeStr} had an invalid hash.", onNewAssetRefCallback);
55+
}
56+
57+
else
58+
{
59+
DisallowedAsset? disallowed = parameters.Database.GetDisallowedAssetInfo(parameters.AssetRef);
60+
if (disallowed != null)
61+
{
62+
logger.LogWarning(BunkumCategory.UserContent, $"{parameters.User} tried to use a manually disallowed {assetTypeStr}.");
63+
return new(Unauthorized, disallowanceInfo: disallowed, onNewAssetRefCallback: onNewAssetRefCallback);
64+
}
65+
66+
string filename = isPSP ? $"psp/{parameters.AssetRef}" : parameters.AssetRef;
67+
existsInDataStore = parameters.DataStore.ExistsInStore(filename);
68+
69+
if (!existsInDataStore)
70+
{
71+
logger.LogDebug(BunkumCategory.UserContent, $"Referenced asset '{filename}' could not be found in data store.");
72+
73+
if (parameters.MustBeInDataStoreIfHash)
74+
return new(NotFound, null, $"The used {assetTypeStr} did not exist on the server.", onNewAssetRefCallback);
75+
}
76+
77+
asset = parameters.Cache.GetAssetInfo(parameters.AssetRef, parameters.Database);
78+
79+
// Only try to import if the asset exists in the data store
80+
if (existsInDataStore && asset == null)
81+
{
82+
logger.LogInfo(BunkumCategory.UserContent, $"Referenced asset '{filename}' exists in data store but not in database, attempting to import automatically...");
83+
Stopwatch sw = new();
84+
sw.Start();
85+
86+
if (!parameters.DataStore.TryGetDataFromStore(filename, out byte[]? assetData) || assetData == null)
87+
{
88+
sw.Stop();
89+
logger.LogError(BunkumCategory.UserContent, $"Failed to read '{filename}' from data store!");
90+
logger.LogDebug(BunkumCategory.UserContent, $"Failed to get '{filename}' after {sw.ElapsedMilliseconds}ms.");
91+
return new(InternalServerError, null, $"Failed to read {assetTypeStr} internally. Please report this to the server owner.", onNewAssetRefCallback, existsInDataStore: existsInDataStore);
92+
}
93+
94+
asset = parameters.AssetImporter.ReadAndVerifyAsset(parameters.AssetRef, assetData, parameters.PlatformToUseIn, parameters.Database);
95+
if (asset == null)
96+
{
97+
sw.Stop();
98+
logger.LogDebug(BunkumCategory.UserContent, $"Failed to get '{filename}' after {sw.ElapsedMilliseconds}ms.");
99+
return new(BadRequest, null, $"The used {assetTypeStr} was invalid or corrupt.", onNewAssetRefCallback, existsInDataStore: existsInDataStore);
100+
}
101+
102+
sw.Stop();
103+
logger.LogInfo(BunkumCategory.UserContent, $"Successfully imported '{filename}' in {sw.ElapsedMilliseconds}ms.");
104+
}
105+
106+
// FIXME: for some reason, PSP texture detection/conversion broke so we can no longer tell if a PSP texture is actually a texture, so skip this for PSP
107+
if (asset != null && !isPSP)
108+
{
109+
bool isHashedTexture = (asset.AssetFlags & AssetFlags.Imagery) != 0;
110+
111+
if (parameters.MustBeTexture && !isHashedTexture)
112+
return new(BadRequest, null, $"The used {assetTypeStr} was not a valid custom image.", onNewAssetRefCallback, assetInfo: asset, existsInDataStore: existsInDataStore);
113+
114+
// TODO: actually use AIPI to scan image if not null
115+
}
116+
}
117+
118+
return new(OK, parameters.AssetRef, null, onNewAssetRefCallback, assetInfo: asset, existsInDataStore: existsInDataStore);
119+
}
120+
}

Refresh.Core/Importing/Importer.cs

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -107,32 +107,40 @@ private static bool MatchesMagic(ReadOnlySpan<byte> data, ulong magic)
107107
/// <returns>Whether the file is likely of TGA format</returns>
108108
private static bool IsPspTga(ReadOnlySpan<byte> data)
109109
{
110-
byte imageIdLength = data[0];
111-
byte colorMapType = data[1];
112-
byte imageType = data[2];
113-
ReadOnlySpan<byte> colorMapSpecification = data[3..8];
114-
ReadOnlySpan<byte> imageSpecification = data[8..18];
115-
short xOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[..2]);
116-
short yOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[2..4]);
117-
ushort width = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[4..6]);
118-
ushort height = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[6..8]);
119-
byte depth = imageSpecification[8];
120-
byte descriptor = imageSpecification[9];
121-
122-
//PSP does not seem to fill out this information
123-
if (imageIdLength != 0) return false;
124-
if (xOrigin != 0) return false;
125-
if (yOrigin != 0) return false;
126-
//These are the fields set by PSP, that shouldn't change from image to image
127-
if (colorMapType != 1) return false;
128-
if (descriptor != 0) return false;
129-
if (imageType != 1) return false;
130-
if (depth != 8) return false;
131-
//Reasonable validation checks (PSP seems to only send images of max size 480x272)
132-
if (width > 500) return false;
133-
if (height > 300) return false;
134-
135-
return true;
110+
try
111+
{
112+
byte imageIdLength = data[0];
113+
byte colorMapType = data[1];
114+
byte imageType = data[2];
115+
ReadOnlySpan<byte> colorMapSpecification = data[3..8];
116+
ReadOnlySpan<byte> imageSpecification = data[8..18];
117+
short xOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[..2]);
118+
short yOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[2..4]);
119+
ushort width = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[4..6]);
120+
ushort height = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[6..8]);
121+
byte depth = imageSpecification[8];
122+
byte descriptor = imageSpecification[9];
123+
124+
//PSP does not seem to fill out this information
125+
if (imageIdLength != 0) return false;
126+
if (xOrigin != 0) return false;
127+
if (yOrigin != 0) return false;
128+
//These are the fields set by PSP, that shouldn't change from image to image
129+
if (colorMapType != 1) return false;
130+
if (descriptor != 0) return false;
131+
if (imageType != 1) return false;
132+
if (depth != 8) return false;
133+
//Reasonable validation checks (PSP seems to only send images of max size 480x272)
134+
if (width > 500) return false;
135+
if (height > 300) return false;
136+
137+
return true;
138+
}
139+
catch
140+
{
141+
//If the data couldn't be read, it's not a TGA
142+
return false;
143+
}
136144
}
137145

138146
private bool IsMip(Span<byte> rawData)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using Bunkum.Core.Storage;
2+
using Refresh.Core.Importing;
3+
using Refresh.Core.Services;
4+
using Refresh.Core.Types.Data;
5+
using Refresh.Database;
6+
using Refresh.Database.Models.Authentication;
7+
using Refresh.Database.Models.Users;
8+
9+
namespace Refresh.Core.Types.Assets.Validation;
10+
11+
public struct AssetValidationParameters
12+
{
13+
/// <summary>
14+
/// The reference (hash/guid/blank) to validate
15+
/// </summary>
16+
public string AssetRef { get; set; } = "0";
17+
public GameUser? User { get; set; }
18+
public TokenGame GameToUseIn { get; set; }
19+
public TokenPlatform PlatformToUseIn { get; set; }
20+
public GameDatabaseContext Database { get; set; } = null!;
21+
public IDataStore DataStore { get; set; } = null!;
22+
public CacheService Cache { get; set; } = null!;
23+
public GuidCheckerService GuidChecker { get; set; } = null!;
24+
public AssetImporter AssetImporter { get; set; } = null!;
25+
public AipiService? Aipi { get; set; }
26+
27+
public bool MayBeBlank { get; set; } = true;
28+
public bool MayBeGuid { get; set; } = true;
29+
public bool MayBeHash { get; set; } = true;
30+
public bool MustBeInDataStoreIfHash { get; set; } = true;
31+
public bool MustBeTexture { get; set; } = false;
32+
33+
/// <summary>
34+
/// What the asset should be referred as in user-faced error messages and in logs, e.g. "planet asset" or "icon".
35+
/// If null, we will default to calling it "asset" or "image" depending on MustBeTexture.
36+
/// </summary>
37+
public string? AssetContextTypeStr { get; set; }
38+
39+
/// <summary>
40+
/// Callback which is called with the new asset reference as parameter when constructing <see cref="ValidatedAssetResult"/>;
41+
/// useful to update asset references of entities during validation without requiring the caller to manually reassign them after validation;
42+
/// this way similar attributes like photo images or face icons can simply be iterated.
43+
/// If null, this will be skipped.
44+
/// </summary>
45+
public Action<string>? OnNewAssetRefCallback { get; set; }
46+
47+
public AssetValidationParameters(string assetKey, DataContext dataContext, AssetImporter assetImporter, AipiService? aipi = null)
48+
{
49+
this.AssetRef = assetKey;
50+
this.User = dataContext.User;
51+
this.GameToUseIn = dataContext.Game;
52+
this.PlatformToUseIn = dataContext.Platform;
53+
this.Database = dataContext.Database;
54+
this.DataStore = dataContext.DataStore;
55+
this.Cache = dataContext.Cache;
56+
this.GuidChecker = dataContext.GuidChecker;
57+
this.AssetImporter = assetImporter;
58+
this.Aipi = aipi;
59+
}
60+
61+
public AssetValidationParameters()
62+
{
63+
64+
}
65+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Net;
2+
using Refresh.Database.Models.Assets;
3+
4+
namespace Refresh.Core.Types.Assets.Validation;
5+
6+
public struct ValidatedAssetResult
7+
{
8+
/// <summary>
9+
/// HTTP code to return if validation failed.
10+
/// OK: don't cancel request and proceed.
11+
/// </summary>
12+
public HttpStatusCode Status { get; set; }
13+
14+
/// <summary>
15+
/// new reference (hash/guid/blank) to use
16+
/// </summary>
17+
public string NewAssetRef { get; set; }
18+
19+
/// <summary>
20+
/// message to show to the user.
21+
/// null: don't show anything.
22+
/// </summary>
23+
public string? ErrorMessage { get; set; }
24+
25+
public GameAsset? AssetInfo { get; set; }
26+
public DisallowedAsset? DisallowanceInfo { get; set; }
27+
public bool ExistsInDataStore { get; set; }
28+
29+
public ValidatedAssetResult(HttpStatusCode status, string? newAssetRef = null, string? errorMessage = null, Action<string>? onNewAssetRefCallback = null,
30+
GameAsset? assetInfo = null, DisallowedAsset? disallowanceInfo = null, bool existsInDataStore = false)
31+
{
32+
this.Status = status;
33+
this.NewAssetRef = newAssetRef ?? "0";
34+
this.ErrorMessage = errorMessage;
35+
this.AssetInfo = assetInfo;
36+
this.DisallowanceInfo = disallowanceInfo;
37+
this.ExistsInDataStore = existsInDataStore;
38+
39+
onNewAssetRefCallback?.Invoke(this.NewAssetRef);
40+
}
41+
}

Refresh.Database/GameDatabaseContext.Assets.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,30 @@ public void SetMainlinePhotoHash(GameAsset asset, string hash) =>
111111
asset.AsMainlinePhotoHash = hash;
112112
});
113113

114+
public bool IsAssetDisallowed(string hash)
115+
{
116+
string hashLower = hash.ToLower();
117+
return this.DisallowedAssets.Any(u => u.AssetHash == hashLower);
118+
}
119+
114120
public DisallowedAsset? GetDisallowedAssetInfo(string hash)
115-
=> this.DisallowedAssets.FirstOrDefault(d => d.AssetHash == hash);
121+
{
122+
string hashLower = hash.ToLower();
123+
return this.DisallowedAssets.FirstOrDefault(d => d.AssetHash == hashLower);
124+
}
116125

117126
/// <returns>
118127
/// The asset's disallowance info + whether the asset wasn't already disallowed before
119128
/// </returns
120129
public (DisallowedAsset, bool) DisallowAsset(string hash, GameAssetType type, string reason)
121130
{
122-
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hash);
131+
string hashLower = hash.ToLower();
132+
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hashLower);
123133
if (existing != null) return (existing, false);
124134

125135
DisallowedAsset disallowed = new()
126136
{
127-
AssetHash = hash,
137+
AssetHash = hashLower,
128138
AssetType = type,
129139
Reason = reason,
130140
DisallowedAt = this._time.Now,
@@ -137,7 +147,8 @@ public void SetMainlinePhotoHash(GameAsset asset, string hash) =>
137147

138148
public bool ReallowAsset(string hash)
139149
{
140-
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hash);
150+
string hashLower = hash.ToLower();
151+
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hashLower);
141152
if (existing == null) return false;
142153

143154
this.DisallowedAssets.Remove(existing);

Refresh.Database/GameDatabaseContext.Levels.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -597,12 +597,10 @@ public void SetLevelCoolRatings(Dictionary<GameLevel, float> coolRatingsToSet)
597597
this.SaveChanges();
598598
}
599599

600-
public void SetLevelModdedStatus(GameLevel level, bool modded)
600+
public void SetLevelModdedStatus(GameLevel level, bool modded, bool save = true)
601601
{
602-
this.Write(() =>
603-
{
604-
level.IsModded = modded;
605-
});
602+
level.IsModded = modded;
603+
if (save) this.SaveChanges();
606604
}
607605

608606
public void SetLevelModdedStatuses(Dictionary<GameLevel, bool> levels)

0 commit comments

Comments
 (0)