Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions src/scripts/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function toOldArabic(sentence: string, option: OldArabicOptions = {}): st
}

export function toOldArabicAndTashfeerBannedWords(sentence: string, levelOfTashfeer: number = 2): string {
validateTashfeerLevel(levelOfTashfeer);
let new_sentence = '';
const words = sentence.trim().split(' ');
for (const word of words) {
Expand Down Expand Up @@ -176,12 +177,29 @@ export function removeArabicAffixes(text: string): string {
}

/**
* Calculates the encryption level based on the input level and word length.
* Validates the encryption level to guarantee bounded execution and prevent infinite loops.
* @param {unknown} level - The input encryption level.
*/
function validateTashfeerLevel(level: unknown): void {
if (typeof level !== 'number') {
throw new TypeError('Level of tashfeer must be a number');
}
if (!Number.isFinite(level) || !Number.isInteger(level) || level < 0) {
throw new RangeError('Level of tashfeer must be a non-negative finite integer');
}
}

/**
* Calculates the encryption level based on the input level and the length of the word.
*
* @param {number} level - The input encryption level.
* @param {number} wordLength - The length of the word to be encrypted.
* @returns {number} The calculated encryption level.
*/
function calculateEncryptionLevel(level: number, wordLength: number): number {
if (wordLength <= 0) {
return 0;
}
// Check if the word length is less than or equal to 4
if (wordLength <= 4) {
// If so, return the minimum of (1 + level) and the word length
Expand All @@ -204,11 +222,16 @@ function calculateEncryptionLevel(level: number, wordLength: number): number {
* @returns {number[]} An array of random indexes.
*/
function getRandomIndexes(numOfIndexesToNeeded: number, wordLength: number): number[] {
if (wordLength <= 0 || numOfIndexesToNeeded <= 0) {
return [];
}

const count = Math.min(numOfIndexesToNeeded, wordLength);
// Create a Set to store unique random indexes
const randomIndexes = new Set<number>();

// Continue generating random indexes until the desired number is reached
while (randomIndexes.size !== numOfIndexesToNeeded) {
// Continue generating random indexes until the desired count is reached
while (randomIndexes.size < count) {
// Generate a random index within the word length
randomIndexes.add(Math.floor(Math.random() * wordLength));
}
Expand Down Expand Up @@ -299,6 +322,7 @@ function tashfeerHandler(word: string, level: number = 0): string {
* @returns {string} The encrypted sentence.
*/
export function tashfeer(sentence: string, levelOfTashfeer: number = 1): string {
validateTashfeerLevel(levelOfTashfeer);
sentence = sentence.trim();
let new_sentence = '';
for (const word of sentence.split(' ')) {
Expand Down Expand Up @@ -344,6 +368,7 @@ function checkIfBannedWord(string: string): boolean {
* @returns {string} The encrypted sentence with tashfeer applied to banned words.
*/
export function tashfeerBannedWords(sentence: string, levelOfTashfeer: number = 2): string {
validateTashfeerLevel(levelOfTashfeer);
let new_sentence = '';
sentence = sentence.trim();
for (const word of sentence.split(' ')) {
Expand Down
50 changes: 50 additions & 0 deletions tests/edge-cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,56 @@ describe('Arabic NLP Edge Cases & Robustness', () => {
});
});

describe('Tashfeer Level Validation and Nontermination Prevention (#35)', () => {
it('should throw RangeError for negative levels', () => {
expect(() => tashfeer('سلام', -2)).toThrow(RangeError);
expect(() => tashfeer('سلام', -1)).toThrow(RangeError);
expect(() => tashfeerBannedWords('هذا السلاح ممنوع', -1)).toThrow(RangeError);
expect(() => toOldArabicAndTashfeerBannedWords('هذا السلاح ممنوع', -2)).toThrow(RangeError);
});

it('should throw RangeError for fractional levels', () => {
expect(() => tashfeer('سلام', 0.5)).toThrow(RangeError);
expect(() => tashfeer('سلام', 1.2)).toThrow(RangeError);
expect(() => tashfeerBannedWords('هذا السلاح ممنوع', 2.5)).toThrow(RangeError);
expect(() => toOldArabicAndTashfeerBannedWords('هذا السلاح ممنوع', 0.1)).toThrow(RangeError);
});

it('should throw RangeError for NaN and non-finite levels', () => {
expect(() => tashfeer('سلام', NaN)).toThrow(RangeError);
expect(() => tashfeer('سلام', Infinity)).toThrow(RangeError);
expect(() => tashfeer('سلام', -Infinity)).toThrow(RangeError);
expect(() => tashfeerBannedWords('هذا السلاح ممنوع', NaN)).toThrow(RangeError);
expect(() => toOldArabicAndTashfeerBannedWords('هذا السلاح ممنوع', Infinity)).toThrow(RangeError);
});

it('should throw TypeError for non-number levels', () => {
// @ts-expect-error testing JS caller invalid type
expect(() => tashfeer('سلام', '1')).toThrow(TypeError);
// @ts-expect-error testing JS caller invalid type
expect(() => tashfeer('سلام', null)).toThrow(TypeError);
// @ts-expect-error testing JS caller invalid type
expect(() => tashfeer('سلام', {})).toThrow(TypeError);
// @ts-expect-error testing JS caller invalid type
expect(() => tashfeerBannedWords('هذا السلاح ممنوع', '2')).toThrow(TypeError);
// @ts-expect-error testing JS caller invalid type
expect(() => toOldArabicAndTashfeerBannedWords('هذا السلاح ممنوع', null)).toThrow(TypeError);
});

it('should terminate promptly and handle valid level 0, defaults, and large levels', () => {
const start = performance.now();
const resDefault = tashfeer('سلام');
const res0 = tashfeer('سلام', 0);
const resLarge = tashfeer('سلام', 50);
const duration = performance.now() - start;

expect(resDefault).toBeDefined();
expect(res0).toBeDefined();
expect(resLarge).toBeDefined();
expect(duration).toBeLessThan(100);
});
});

describe('Performance & ReDoS Resilience', () => {
it('should process large texts (50,000+ chars) in under 150ms without catastrophic backtracking', () => {
const sampleUnit =
Expand Down
Loading