From a996563f4b40bb579235dfe646341d1b529657bc Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 29 May 2026 23:03:50 +0530 Subject: [PATCH 1/3] Fix crash on lone \sqrt at end of input (#188) The sqrt branch in MTMathListBuilder read the next character unconditionally. When \sqrt was the last token in the input, this hit the NSAssert in getNextCharacter and crashed with an NSInternalInconsistencyException. Guard the lookahead with hasCharacters; when no argument follows, build an empty radicand, matching the existing behavior of \sqrt{}. Adds testSqrtAtEnd covering the regression. Co-Authored-By: Claude Opus 4.8 --- iosMath/lib/MTMathListBuilder.m | 18 ++++++++++++------ iosMathTests/MTMathListBuilderTest.m | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/iosMath/lib/MTMathListBuilder.m b/iosMath/lib/MTMathListBuilder.m index f3a9943..ef8de69 100644 --- a/iosMath/lib/MTMathListBuilder.m +++ b/iosMath/lib/MTMathListBuilder.m @@ -746,13 +746,19 @@ - (MTMathAtom*) atomForCommand:(NSString*) command } else if ([command isEqualToString:@"sqrt"]) { // A sqrt command with one argument MTRadical* rad = [MTRadical new]; - unichar ch = [self getNextCharacter]; - if (ch == '[') { - // special handling for sqrt[degree]{radicand} - rad.degree = [self buildInternal:false stopChar:']']; - rad.radicand = [self buildInternal:true]; + if ([self hasCharacters]) { + unichar ch = [self getNextCharacter]; + if (ch == '[') { + // special handling for sqrt[degree]{radicand} + rad.degree = [self buildInternal:false stopChar:']']; + rad.radicand = [self buildInternal:true]; + } else { + [self unlookCharacter]; + rad.radicand = [self buildInternal:true]; + } } else { - [self unlookCharacter]; + // No argument follows (e.g. a lone "\sqrt" at the end of input). + // Build an empty radicand rather than reading past the end. rad.radicand = [self buildInternal:true]; } return rad; diff --git a/iosMathTests/MTMathListBuilderTest.m b/iosMathTests/MTMathListBuilderTest.m index 36a571c..2780f56 100644 --- a/iosMathTests/MTMathListBuilderTest.m +++ b/iosMathTests/MTMathListBuilderTest.m @@ -378,6 +378,31 @@ - (void) testSqrt XCTAssertEqualObjects(latex, @"\\sqrt{2}", @"%@", desc); } +- (void) testSqrtAtEnd +{ + // A lone \sqrt with no argument at the end of the input must not crash + // (it previously asserted in getNextCharacter). It should parse as a + // radical with an empty radicand, matching \sqrt{}. + NSString *str = @"\\sqrt"; + MTMathList* list = [MTMathListBuilder buildFromString:str]; + NSString* desc = [NSString stringWithFormat:@"Error for string:%@", str]; + + XCTAssertNotNil(list, @"%@", desc); + XCTAssertEqualObjects(@(list.atoms.count), @1, @"%@", desc); + MTRadical* rad = list.atoms[0]; + XCTAssertEqual(rad.type, kMTMathAtomRadical, @"%@", desc); + XCTAssertEqualObjects(rad.nucleus, @"", @"%@", desc); + + MTMathList *subList = rad.radicand; + XCTAssertNotNil(subList, @"%@", desc); + XCTAssertEqualObjects(@(subList.atoms.count), @0, @"%@", desc); + XCTAssertNil(rad.degree, @"%@", desc); + + // convert it back to latex + NSString* latex = [MTMathListBuilder mathListToString:list]; + XCTAssertEqualObjects(latex, @"\\sqrt{}", @"%@", desc); +} + - (void) testSqrtInSqrt { NSString *str = @"\\sqrt\\sqrt2"; From 8236419ad3829698b73bb6c49f94e155a9525772 Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 29 May 2026 23:07:51 +0530 Subject: [PATCH 2/3] Add testSqrtInGroup edge-case test for #188 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers a lone \sqrt inside a group ({\sqrt}), which reaches an empty radicand via the oneCharOnly stop-char guard — a different path than the end-of-input case. Suggested in code review. Co-Authored-By: Claude Opus 4.8 --- iosMathTests/MTMathListBuilderTest.m | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/iosMathTests/MTMathListBuilderTest.m b/iosMathTests/MTMathListBuilderTest.m index 2780f56..573db44 100644 --- a/iosMathTests/MTMathListBuilderTest.m +++ b/iosMathTests/MTMathListBuilderTest.m @@ -403,6 +403,30 @@ - (void) testSqrtAtEnd XCTAssertEqualObjects(latex, @"\\sqrt{}", @"%@", desc); } +- (void) testSqrtInGroup +{ + // A \sqrt with no argument inside a group exercises a different path + // (empty radicand via the oneCharOnly stop-char guard) than the + // end-of-input case, and must also not crash. + NSString *str = @"{\\sqrt}"; + MTMathList* list = [MTMathListBuilder buildFromString:str]; + NSString* desc = [NSString stringWithFormat:@"Error for string:%@", str]; + + XCTAssertNotNil(list, @"%@", desc); + XCTAssertEqualObjects(@(list.atoms.count), @1, @"%@", desc); + MTRadical* rad = list.atoms[0]; + XCTAssertEqual(rad.type, kMTMathAtomRadical, @"%@", desc); + + MTMathList *subList = rad.radicand; + XCTAssertNotNil(subList, @"%@", desc); + XCTAssertEqualObjects(@(subList.atoms.count), @0, @"%@", desc); + XCTAssertNil(rad.degree, @"%@", desc); + + // convert it back to latex + NSString* latex = [MTMathListBuilder mathListToString:list]; + XCTAssertEqualObjects(latex, @"\\sqrt{}", @"%@", desc); +} + - (void) testSqrtInSqrt { NSString *str = @"\\sqrt\\sqrt2"; From d0add7a6ff2098152e85144b5c4a9ba83cabe22e Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 29 May 2026 23:12:39 +0530 Subject: [PATCH 3/3] Simplify \sqrt radicand parsing (#188) Pull the common radicand build out of all three branches into a single call after the optional degree parse, per review on #207. Co-Authored-By: Claude Opus 4.8 --- iosMath/lib/MTMathListBuilder.m | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/iosMath/lib/MTMathListBuilder.m b/iosMath/lib/MTMathListBuilder.m index ef8de69..ab492d4 100644 --- a/iosMath/lib/MTMathListBuilder.m +++ b/iosMath/lib/MTMathListBuilder.m @@ -746,21 +746,18 @@ - (MTMathAtom*) atomForCommand:(NSString*) command } else if ([command isEqualToString:@"sqrt"]) { // A sqrt command with one argument MTRadical* rad = [MTRadical new]; + // Guard against a lone "\sqrt" at the end of input: only read a + // character if one is available. if ([self hasCharacters]) { unichar ch = [self getNextCharacter]; if (ch == '[') { // special handling for sqrt[degree]{radicand} rad.degree = [self buildInternal:false stopChar:']']; - rad.radicand = [self buildInternal:true]; } else { [self unlookCharacter]; - rad.radicand = [self buildInternal:true]; } - } else { - // No argument follows (e.g. a lone "\sqrt" at the end of input). - // Build an empty radicand rather than reading past the end. - rad.radicand = [self buildInternal:true]; } + rad.radicand = [self buildInternal:true]; return rad; } else if ([command isEqualToString:@"left"]) { // Save the current inner while a new one gets built.