@@ -12,6 +12,32 @@ import { importGameBugH } from "@/bug/paste.bug";
1212const BRAINKING_SITE = '[Site "BrainKing.com (Prague, Czech Republic)"]' ;
1313const EMBASSY_FEN = '[FEN "rnbqkmcbnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNBQKMCBNR w KQkq - 0 1"]' ;
1414const BUGHOUSE_VARIANT = '[WhiteA' ;
15+ const IMPORT_FFISH_ERROR_BUFFER : string [ ] = [ ] ;
16+ const FEN_VALIDATION_ERRORS : Record < number , string > = {
17+ [ - 14 ] : 'Invalid counting rule field' ,
18+ [ - 13 ] : 'Invalid check count field' ,
19+ [ - 12 ] : 'Invalid promoted piece marker' ,
20+ [ - 11 ] : 'Invalid number of FEN fields' ,
21+ [ - 10 ] : 'Invalid character in board layout' ,
22+ [ - 9 ] : 'Touching kings are not allowed' ,
23+ [ - 8 ] : 'Invalid board geometry' ,
24+ [ - 7 ] : 'Invalid pocket information' ,
25+ [ - 6 ] : 'Invalid side to move field' ,
26+ [ - 5 ] : 'Invalid castling information' ,
27+ [ - 4 ] : 'Invalid en-passant square' ,
28+ [ - 3 ] : 'Invalid number of kings' ,
29+ [ - 2 ] : 'Invalid half-move counter' ,
30+ [ - 1 ] : 'Invalid move counter' ,
31+ [ 0 ] : 'Empty FEN' ,
32+ } ;
33+
34+ export function recordImportFfishError ( text : string ) : void {
35+ const message = text . trim ( ) ;
36+ if ( / ^ V a r i a n t ' .* ' a l r e a d y e x i s t s \. $ / . test ( message ) ) return ;
37+ console . warn ( message ) ;
38+ IMPORT_FFISH_ERROR_BUFFER . push ( message ) ;
39+ if ( IMPORT_FFISH_ERROR_BUFFER . length > 200 ) IMPORT_FFISH_ERROR_BUFFER . shift ( ) ;
40+ }
1541
1642
1743export function pasteView ( model : PyChessModel ) : VNode [ ] {
@@ -52,8 +78,11 @@ export function pasteView(model: PyChessModel): VNode[] {
5278 ffish . loadVariantConfig ( variantsIni ) ;
5379 const XHR = new XMLHttpRequest ( ) ;
5480 const FD = new FormData ( ) ;
81+ const ffishErrorStart = IMPORT_FFISH_ERROR_BUFFER . length ;
5582
56- let variant , initialFen , board ;
83+ let variant : string ;
84+ let initialFen : string ;
85+ let board ;
5786 let mainlineMoves : string [ ] = [ ] ;
5887
5988 try {
@@ -79,17 +108,15 @@ export function pasteView(model: PyChessModel): VNode[] {
79108
80109 for ( let idx = 0 ; idx < moves . length ; ++ idx ) {
81110 move = moves [ idx ] ;
82- try {
83- board . push ( move ) ;
84- mainlineMoves . push ( move ) ;
85- }
86- catch ( err ) {
111+ const pushed = board . push ( move ) ;
112+ if ( ! pushed ) {
87113 alert ( 'Illegal move ' + move ) ;
88114 status = 10 ;
89115 // LOSS for the moving player
90116 result = resultString ( false , idx + 1 , isHandicap ) ;
91117 break ;
92118 }
119+ mainlineMoves . push ( move ) ;
93120 }
94121
95122 FD . append ( 'Variant' , 'shogi' ) ;
@@ -108,11 +135,13 @@ export function pasteView(model: PyChessModel): VNode[] {
108135 } else {
109136
110137 const game = ffish . readGamePGN ( pgn ) ;
138+ const parserError = getLatestFfishError ( ffishErrorStart ) ;
139+ if ( parserError ) {
140+ throw new Error ( parserError ) ;
141+ }
111142
112- variant = "chess" ;
113- const v = game . headers ( "Variant" ) ;
114- //console.log("Variant:", v);
115- if ( v ) variant = v . toLowerCase ( ) ;
143+ const variantInfo = parseVariantTag ( game . headers ( "Variant" ) ) ;
144+ variant = variantInfo . variant ;
116145
117146 if ( variant === 'alice' ) {
118147 // TODO
@@ -121,33 +150,42 @@ export function pasteView(model: PyChessModel): VNode[] {
121150 alert ( error ) ;
122151 return ;
123152 }
153+ if ( ! ( variant in VARIANTS ) ) {
154+ throw new Error ( `Unsupported PGN Variant tag: ${ variantInfo . raw } ` ) ;
155+ }
124156
125157 initialFen = VARIANTS [ variant ] . startFen ;
126158 const f = game . headers ( "FEN" ) ;
127- if ( f ) initialFen = f ;
159+ if ( f ) {
160+ const fenValidation = validateFenTag ( ffish , f , variantInfo . variant , variantInfo . chess960 ) ;
161+ if ( fenValidation !== null ) {
162+ throw new Error ( fenValidation ) ;
163+ }
164+ initialFen = f ;
165+ }
128166
129167 const t = game . headers ( "Termination" ) ;
130168 //console.log("Termination:", t);
131169 if ( t ) {
132- status = getStatus ( t . toLowerCase ( ) ) ;
170+ const status = getStatus ( t . toLowerCase ( ) ) ;
133171 FD . append ( 'Status' , "" + status ) ;
134172 }
135173
136- // TODO: crazyhouse960 but without 960? (export to lichess hack)
137- const is960 = variant . includes ( "960" ) || variant . includes ( 'random' ) ;
138-
139- board = new ffish . Board ( variant , initialFen , is960 ) ;
174+ board = new ffish . Board ( variant , initialFen , variantInfo . chess960 ) ;
140175
141- mainlineMoves = game . mainlineMoves ( ) . split ( " " ) ;
176+ mainlineMoves = game . mainlineMoves ( ) . split ( / \s + / ) . filter ( ( move : string ) => move . length > 0 ) ;
142177 for ( let idx = 0 ; idx < mainlineMoves . length ; ++ idx ) {
143- board . push ( mainlineMoves [ idx ] ) ;
178+ const pushed = board . push ( mainlineMoves [ idx ] ) ;
179+ if ( ! pushed ) {
180+ throw new Error ( `Illegal move at ply ${ idx + 1 } : ${ mainlineMoves [ idx ] } ` ) ;
181+ }
144182 }
145183
146184 const tags = ( game . headerKeys ( ) as string ) . split ( ' ' ) ;
147185 tags . forEach ( ( tag ) => {
148186 FD . append ( tag , game . headers ( tag ) ) ;
149187 } ) ;
150- FD . append ( 'moves' , game . mainlineMoves ( ) ) ;
188+ FD . append ( 'moves' , mainlineMoves . join ( ' ' ) ) ;
151189 FD . append ( 'final_fen' , board . fen ( ) ) ;
152190 FD . append ( 'username' , model [ "username" ] ) ;
153191
@@ -156,22 +194,42 @@ export function pasteView(model: PyChessModel): VNode[] {
156194 }
157195 }
158196 catch ( err ) {
159- e . setCustomValidity ( err . message ? _ ( 'Invalid PGN' ) : '' ) ;
160- alert ( err ) ;
197+ const message = buildImportErrorMessage ( err , pgn , ffish ) ;
198+ e . setCustomValidity ( message ) ;
199+ alert ( message ) ;
161200 return ;
162201 }
163202
164203 XHR . onreadystatechange = function ( ) {
165- if ( this . readyState === 4 && this . status === 200 ) {
166- const response = JSON . parse ( this . responseText ) ;
204+ if ( this . readyState !== 4 ) return ;
205+
206+ let response : Record < string , string > = { } ;
207+ if ( this . responseText ) {
208+ try {
209+ response = JSON . parse ( this . responseText ) ;
210+ } catch ( _err ) {
211+ response = { } ;
212+ }
213+ }
214+
215+ if ( this . status === 200 ) {
167216 if ( response [ 'gameId' ] !== undefined ) {
168217 window . location . assign ( model [ "home" ] + '/' + response [ 'gameId' ] ) ;
169- } else if ( response [ 'error' ] !== undefined ) {
218+ return ;
219+ }
220+ if ( response [ 'error' ] !== undefined ) {
170221 alert ( response [ 'error' ] ) ;
222+ return ;
171223 }
224+ alert ( _ ( 'Import failed' ) ) ;
225+ return ;
172226 }
227+
228+ alert ( response [ 'error' ] ?? `${ _ ( 'Import failed' ) } (${ this . status } )` ) ;
229+ } ;
230+ XHR . onerror = function ( ) {
231+ alert ( _ ( 'Import failed' ) ) ;
173232 } ;
174- console . log ( FD ) ;
175233 XHR . open ( "POST" , "/import" , true ) ;
176234 XHR . send ( FD ) ;
177235 }
@@ -217,3 +275,68 @@ function getStatus(termination: string) {
217275 if ( termination . includes ( 'abandon' ) ) return '7' ;
218276 return '11' ; // unknown
219277}
278+
279+ function parseVariantTag ( rawVariant : string ) : { variant : string ; chess960 : boolean ; raw : string } {
280+ const raw = rawVariant || 'chess' ;
281+ let variant = raw . toLowerCase ( ) ;
282+ let chess960 = variant . includes ( "960" ) || variant . includes ( 'random' ) ;
283+
284+ variant = variant . endsWith ( '960' ) ? variant . slice ( 0 , - 3 ) : variant ;
285+ if ( variant === "caparandom" ) {
286+ variant = "capablanca" ;
287+ chess960 = true ;
288+ } else if ( variant === "fischerandom" ) {
289+ variant = "chess" ;
290+ chess960 = true ;
291+ }
292+ return { variant, chess960, raw } ;
293+ }
294+
295+ function validateFenTag ( ffish : FairyStockfish , fen : string , variant : string , chess960 : boolean ) : string | null {
296+ const validationCode = ffish . validateFen ( fen , variant , chess960 ) ;
297+ if ( validationCode === 1 ) return null ;
298+
299+ const details = FEN_VALIDATION_ERRORS [ validationCode ] ?? 'Unknown FEN validation error' ;
300+ return `Invalid [FEN] tag (code ${ validationCode } ): ${ details } .` ;
301+ }
302+
303+ function getLatestFfishError ( fromIndex : number ) : string | null {
304+ if ( IMPORT_FFISH_ERROR_BUFFER . length <= fromIndex ) return null ;
305+ const latest = IMPORT_FFISH_ERROR_BUFFER [ IMPORT_FFISH_ERROR_BUFFER . length - 1 ] ;
306+ return latest && latest . trim ( ) ? latest . trim ( ) : null ;
307+ }
308+
309+ function extractPgnTags ( pgn : string ) : Record < string , string > {
310+ const tags : Record < string , string > = { } ;
311+ const regex = / ^ \s * \[ ( [ A - Z a - z 0 - 9 _ ] + ) \s + " ( (?: [ ^ " \\ ] | \\ .) * ) " \] \s * $ / gm;
312+ let match : RegExpExecArray | null ;
313+ while ( ( match = regex . exec ( pgn ) ) !== null ) {
314+ tags [ match [ 1 ] ] = match [ 2 ] . replace ( / \\ " / g, '"' ) ;
315+ }
316+ return tags ;
317+ }
318+
319+ function buildImportErrorMessage ( err : unknown , pgn : string , ffish : FairyStockfish ) : string {
320+ const tags = extractPgnTags ( pgn ) ;
321+ const variantInfo = parseVariantTag ( tags [ "Variant" ] ?? "chess" ) ;
322+
323+ if ( ! ( variantInfo . variant in VARIANTS ) ) {
324+ return `Unsupported PGN Variant tag: ${ variantInfo . raw } .` ;
325+ }
326+
327+ const fen = tags [ "FEN" ] ;
328+ if ( fen ) {
329+ const fenValidation = validateFenTag ( ffish , fen , variantInfo . variant , variantInfo . chess960 ) ;
330+ if ( fenValidation !== null ) return fenValidation ;
331+ }
332+
333+ const errorMessage =
334+ err instanceof Error
335+ ? err . message
336+ : ( typeof err === "string" ? err : "" ) ;
337+ if ( ! errorMessage ) return _ ( 'Invalid PGN' ) ;
338+ if ( errorMessage . includes ( "memory access out of bounds" ) ) {
339+ return "Failed to parse PGN. Check [Variant], [FEN], and move text formatting." ;
340+ }
341+ return errorMessage ;
342+ }
0 commit comments