Skip to content

Commit 553a981

Browse files
committed
Block invalid language tags
1 parent 68e38d2 commit 553a981

5 files changed

Lines changed: 67 additions & 26 deletions

File tree

src/Machine/src/Serval.Machine.Shared/Services/LanguageTagService.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
namespace Serval.Machine.Shared.Services;
22

3-
public class LanguageTagService : ILanguageTagService
3+
public partial class LanguageTagService : ILanguageTagService
44
{
5+
[GeneratedRegex("^[A-Za-z0-9_-]+$")]
6+
private static partial Regex BroadlyPermissibleLanguageTagPattern();
7+
58
private readonly HashSet<string> _flores200Languages = [];
69
private readonly HashSet<string> _flores200Scripts = [];
710
private readonly LanguageTagParser _parser = new();
@@ -40,9 +43,16 @@ private void InitializeFlores200Languages()
4043
public Flores200Support ConvertToFlores200Code(string languageTag, out string flores200Code)
4144
{
4245
if (_parser.TryParse(languageTag, out string? languageCode, out string? scriptCode))
46+
{
4347
flores200Code = $"{languageCode}_{scriptCode}";
48+
}
4449
else
50+
{
51+
if (!BroadlyPermissibleLanguageTagPattern().IsMatch(languageTag))
52+
throw new InvalidOperationException($"Invalid language tag: {languageTag}");
4553
flores200Code = languageTag;
54+
}
55+
4656
if (_flores200Scripts.Contains(scriptCode ?? ""))
4757
{
4858
if (_flores200Languages.Contains(flores200Code))

src/Machine/test/Serval.Machine.Shared.Tests/Services/LanguageTagServiceTests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ public void OneTimeSetUp()
1010
Sldr.Initialize();
1111
}
1212

13-
[Test]
1413
[TestCase("es", "spa_Latn", Description = "Iso639_1Code")]
1514
[TestCase("hne", "hne_Deva", Description = "Iso639_3Code")]
1615
[TestCase("ks-Arab", "kas_Arab", Description = "ScriptCode")]
@@ -27,13 +26,19 @@ public void OneTimeSetUp()
2726
[TestCase("eng-Latn", "eng_Latn", Description = "DashToUnderscore")]
2827
[TestCase("kor", "kor_Hang", Description = "KoreanScript")]
2928
[TestCase("kor_Kore", "kor_Hang", Description = "KoreanScriptCorrection")]
29+
[TestCase("unknown", "unknown", Description = "UnknownLanguage")]
3030
public void ConvertToFlores200CodeTest(string language, string internalCodeTruth)
3131
{
3232
new LanguageTagService().ConvertToFlores200Code(language, out string internalCode);
3333
Assert.That(internalCode, Is.EqualTo(internalCodeTruth));
3434
}
3535

3636
[Test]
37+
public void ConvertToFlores200CodeTest_InvalidLanguage() =>
38+
Assert.Throws<InvalidOperationException>(() =>
39+
new LanguageTagService().ConvertToFlores200Code("'Invalid Language'", out string _)
40+
);
41+
3742
[TestCase("en", "eng_Latn", Flores200Support.LanguageAndScript)]
3843
[TestCase("ms", "zsm_Latn", Flores200Support.LanguageAndScript)]
3944
[TestCase("cmn", "zho_Hans", Flores200Support.LanguageAndScript)]

src/Serval/src/Serval.Client/Client.g.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,7 +2186,7 @@ public partial interface ITranslationEngineTypesClient
21862186

21872187
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
21882188
/// <summary>
2189-
/// Get information regarding a language for a given engine type
2189+
/// Get information regarding a language for a given engine type.
21902190
/// </summary>
21912191
/// <remarks>
21922192
/// This endpoint exists primarily to support `nmt` model-training since `echo` and `smt-transfer` engines support all languages equally. Given a language tag, it provides the ISO 639-3 code that the tag maps to internally
@@ -2199,9 +2199,9 @@ public partial interface ITranslationEngineTypesClient
21992199
/// <br/>* **`isNative`**: Whether the base translation model supports this language without fine-tuning.
22002200
/// <br/>* **`internalCode`**: The translation model's internal language code. See more details about how the language tag is mapped to an internal code [here](https://github.com/sillsdev/serval/wiki/FLORES%E2%80%90200-Language-Code-Resolution-for-NMT-Engine).
22012201
/// </remarks>
2202-
/// <param name="engineType">A valid engine type: nmt, echo, or smt-transfer</param>
2202+
/// <param name="engineType">A valid engine type: nmt, echo, or smt-transfer.</param>
22032203
/// <param name="language">The language to retrieve information on.</param>
2204-
/// <returns>Language information for the specified engine type</returns>
2204+
/// <returns>Language information for the specified engine type.</returns>
22052205
/// <exception cref="ServalApiException">A server side error occurred.</exception>
22062206
System.Threading.Tasks.Task<LanguageInfo> GetLanguageInfoAsync(string engineType, string language, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
22072207

@@ -2266,7 +2266,7 @@ public string BaseUrl
22662266

22672267
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
22682268
/// <summary>
2269-
/// Get information regarding a language for a given engine type
2269+
/// Get information regarding a language for a given engine type.
22702270
/// </summary>
22712271
/// <remarks>
22722272
/// This endpoint exists primarily to support `nmt` model-training since `echo` and `smt-transfer` engines support all languages equally. Given a language tag, it provides the ISO 639-3 code that the tag maps to internally
@@ -2279,9 +2279,9 @@ public string BaseUrl
22792279
/// <br/>* **`isNative`**: Whether the base translation model supports this language without fine-tuning.
22802280
/// <br/>* **`internalCode`**: The translation model's internal language code. See more details about how the language tag is mapped to an internal code [here](https://github.com/sillsdev/serval/wiki/FLORES%E2%80%90200-Language-Code-Resolution-for-NMT-Engine).
22812281
/// </remarks>
2282-
/// <param name="engineType">A valid engine type: nmt, echo, or smt-transfer</param>
2282+
/// <param name="engineType">A valid engine type: nmt, echo, or smt-transfer.</param>
22832283
/// <param name="language">The language to retrieve information on.</param>
2284-
/// <returns>Language information for the specified engine type</returns>
2284+
/// <returns>Language information for the specified engine type.</returns>
22852285
/// <exception cref="ServalApiException">A server side error occurred.</exception>
22862286
public virtual async System.Threading.Tasks.Task<LanguageInfo> GetLanguageInfoAsync(string engineType, string language, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
22872287
{
@@ -2341,22 +2341,34 @@ public string BaseUrl
23412341
return objectResponse_.Object;
23422342
}
23432343
else
2344+
if (status_ == 400)
2345+
{
2346+
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
2347+
throw new ServalApiException("The language is not valid.", status_, responseText_, headers_, null);
2348+
}
2349+
else
23442350
if (status_ == 401)
23452351
{
23462352
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
2347-
throw new ServalApiException("The client is not authenticated", status_, responseText_, headers_, null);
2353+
throw new ServalApiException("The client is not authenticated.", status_, responseText_, headers_, null);
23482354
}
23492355
else
23502356
if (status_ == 403)
23512357
{
23522358
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
2353-
throw new ServalApiException("The authenticated client cannot perform the operation", status_, responseText_, headers_, null);
2359+
throw new ServalApiException("The authenticated client cannot perform the operation.", status_, responseText_, headers_, null);
2360+
}
2361+
else
2362+
if (status_ == 404)
2363+
{
2364+
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
2365+
throw new ServalApiException("The engine type is not valid.", status_, responseText_, headers_, null);
23542366
}
23552367
else
23562368
if (status_ == 405)
23572369
{
23582370
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
2359-
throw new ServalApiException("The method is not supported", status_, responseText_, headers_, null);
2371+
throw new ServalApiException("The method is not supported.", status_, responseText_, headers_, null);
23602372
}
23612373
else
23622374
{

src/Serval/src/Serval.Translation/Features/EngineTypes/GetLanguageInfo.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task<GetLanguageInfoResponse> HandleAsync(GetLanguageInfo request,
3838
public partial class TranslationEngineTypesController
3939
{
4040
/// <summary>
41-
/// Get information regarding a language for a given engine type
41+
/// Get information regarding a language for a given engine type.
4242
/// </summary>
4343
/// <remarks>
4444
/// This endpoint exists primarily to support `nmt` model-training since `echo` and `smt-transfer` engines support all languages equally. Given a language tag, it provides the ISO 639-3 code that the tag maps to internally
@@ -51,18 +51,22 @@ public partial class TranslationEngineTypesController
5151
/// * **`isNative`**: Whether the base translation model supports this language without fine-tuning.
5252
/// * **`internalCode`**: The translation model's internal language code. See more details about how the language tag is mapped to an internal code [here](https://github.com/sillsdev/serval/wiki/FLORES%E2%80%90200-Language-Code-Resolution-for-NMT-Engine).
5353
/// </remarks>
54-
/// <param name="engineType">A valid engine type: nmt, echo, or smt-transfer</param>
54+
/// <param name="engineType">A valid engine type: nmt, echo, or smt-transfer.</param>
5555
/// <param name="language">The language to retrieve information on.</param>
5656
/// <param name="cancellationToken"></param>
57-
/// <response code="200">Language information for the specified engine type</response>
58-
/// <response code="401">The client is not authenticated</response>
59-
/// <response code="403">The authenticated client cannot perform the operation</response>
60-
/// <response code="405">The method is not supported</response>
57+
/// <response code="200">Language information for the specified engine type.</response>
58+
/// <response code="400">The language is not valid.</response>
59+
/// <response code="401">The client is not authenticated.</response>
60+
/// <response code="403">The authenticated client cannot perform the operation.</response>
61+
/// <response code="404">The engine type is not valid.</response>
62+
/// <response code="405">The method is not supported.</response>
6163
[Authorize(Scopes.ReadTranslationEngines)]
6264
[HttpGet("{engineType}/languages/{language}")]
6365
[ProducesResponseType(StatusCodes.Status200OK)]
66+
[ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)]
6467
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
6568
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
69+
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
6670
[ProducesResponseType(typeof(void), StatusCodes.Status405MethodNotAllowed)]
6771
public async Task<ActionResult<LanguageInfoDto>> GetLanguageInfoAsync(
6872
[NotNull] string engineType,

src/Serval/test/Serval.ApiServer.IntegrationTests/TranslationEngineTests.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,24 +2414,31 @@ public void GetQueueAsync_NotAuthorized()
24142414
public async Task GetLanguageInfoAsync(string engineType)
24152415
{
24162416
TranslationEngineTypesClient client = _env.CreateTranslationEngineTypesClient();
2417-
Client.LanguageInfo languageInfo = await client.GetLanguageInfoAsync(engineType, "Alphabet");
2418-
Assert.Multiple(() =>
2417+
LanguageInfo languageInfo = await client.GetLanguageInfoAsync(engineType, "Alphabet");
2418+
using (Assert.EnterMultipleScope())
24192419
{
24202420
Assert.That(languageInfo.InternalCode, Is.EqualTo("abc_123"));
2421-
Assert.That(languageInfo.IsNative, Is.EqualTo(true));
2422-
});
2421+
Assert.That(languageInfo.IsNative, Is.True);
2422+
}
24232423
}
24242424

2425-
[Test]
2426-
public void GetLanguageInfo_Error()
2425+
[TestCase(new[] { Scopes.ReadTranslationEngines }, "Nmt", "invalid_language", StatusCodes.Status400BadRequest)]
2426+
[TestCase(new[] { Scopes.ReadFiles }, "Nmt", "abc", StatusCodes.Status403Forbidden)]
2427+
[TestCase(new[] { Scopes.ReadTranslationEngines }, "invalid_engine", "en", StatusCodes.Status404NotFound)]
2428+
public void GetLanguageInfo_Error(
2429+
IEnumerable<string> scope,
2430+
string engineType,
2431+
string language,
2432+
int expectedStatusCode
2433+
)
24272434
{
2428-
TranslationEngineTypesClient client = _env.CreateTranslationEngineTypesClient([Scopes.ReadFiles]);
2435+
TranslationEngineTypesClient client = _env.CreateTranslationEngineTypesClient(scope);
24292436
ServalApiException? ex = Assert.ThrowsAsync<ServalApiException>(async () =>
24302437
{
2431-
Client.LanguageInfo languageInfo = await client.GetLanguageInfoAsync("Nmt", "abc");
2438+
_ = await client.GetLanguageInfoAsync(engineType, language);
24322439
});
24332440
Assert.That(ex, Is.Not.Null);
2434-
Assert.That(ex.StatusCode, Is.EqualTo(403));
2441+
Assert.That(ex?.StatusCode, Is.EqualTo(expectedStatusCode));
24352442
}
24362443

24372444
[Test]
@@ -2747,6 +2754,9 @@ public TestEnvironment()
27472754
NmtService
27482755
.GetLanguageInfoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
27492756
.Returns(Task.FromResult(languageInfo));
2757+
NmtService
2758+
.GetLanguageInfoAsync(Arg.Is("invalid_language"), Arg.Any<CancellationToken>())
2759+
.Returns(Task.FromException<LanguageInfoContract>(new InvalidOperationException()));
27502760

27512761
SmtService = Substitute.For<ITranslationEngineService>();
27522762
SmtService

0 commit comments

Comments
 (0)