xref: /aoo41x/main/tools/source/fsys/unx.cxx (revision 89b56da7)
1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 
23 
24 // MARKER(update_precomp.py): autogen include statement, do not remove
25 #include "precompiled_tools.hxx"
26 
27 #include <stdio.h>
28 #include <ctype.h>
29 #include <stdlib.h>
30 #include <unistd.h>
31 #include <utime.h>
32 #if defined HPUX || defined LINUX
33 #include <mntent.h>
34 #define mnttab mntent
35 #elif defined SCO
36 #include <mnttab.h>
37 #elif defined AIX
38 #include <sys/mntctl.h>
39 #include <sys/vmount.h>
40 extern "C" int mntctl( int cmd, size_t size, char* buf );
41 #elif defined(NETBSD)
42 #include <sys/mount.h>
43 #elif defined(FREEBSD) || defined(MACOSX)
44 #elif defined DECUNIX
45 struct mnttab
46 {
47   char *mnt_dir;
48   char *mnt_fsname;
49 };
50 #else
51 #include <sys/mnttab.h>
52 #endif
53 
54 #ifndef MAXPATHLEN
55 #define MAXPATHLEN 1024
56 #endif
57 
58 #include <tools/debug.hxx>
59 #include <tools/list.hxx>
60 #include <tools/fsys.hxx>
61 #include "comdep.hxx"
62 #include <rtl/instance.hxx>
63 
64 DECLARE_LIST( DirEntryList, DirEntry* )
65 DECLARE_LIST( FSysSortList, FSysSort* )
66 DECLARE_LIST( FileStatList, FileStat* )
67 
68 #if defined SOLARIS || defined SINIX
69 #define MOUNTSPECIAL mnt_special
70 #define MOUNTPOINT 	 mnt_mountp
71 #define MOUNTOPTS    mnt_mntopts
72 #define MOUNTFS      mnt_fstype
73 #elif defined SCO
74 #define MNTTAB 		 "/etc/mnttab"
75 #define MOUNTSPECIAL mt_dev
76 #define MOUNTPOINT   mt_filsys
77 #else
78 #define MOUNTSPECIAL mnt_fsname
79 #define MOUNTPOINT   mnt_dir
80 #define MOUNTFS      mnt_type
81 #endif
82 
83 struct mymnttab
84 {
85 	dev_t mountdevice;
86 	ByteString mountspecial;
87 	ByteString mountpoint;
88 	ByteString mymnttab_filesystem;
mymnttabmymnttab89 	mymnttab() { mountdevice = (dev_t) -1; }
90 };
91 
92 
93 #if defined(NETBSD) || defined(FREEBSD) || defined(MACOSX)
GetMountEntry(dev_t,struct mymnttab *)94 sal_Bool GetMountEntry(dev_t /* dev */, struct mymnttab * /* mytab */ )
95 {
96 	DBG_WARNING( "Sorry, not implemented: GetMountEntry" );
97 	return sal_False;
98 }
99 
100 #elif defined AIX
GetMountEntry(dev_t dev,struct mymnttab * mytab)101 sal_Bool GetMountEntry(dev_t dev, struct mymnttab *mytab)
102 {
103 	int bufsize;
104 	if (mntctl (MCTL_QUERY, sizeof bufsize, (char*) &bufsize))
105 		return sal_False;
106 
107 	char* buffer = (char *)malloc( bufsize * sizeof(char) );
108 	if (mntctl (MCTL_QUERY, bufsize, buffer) != -1)
109 		for ( char* vmt = buffer;
110 					vmt < buffer + bufsize;
111 					vmt += ((struct vmount*)vmt)->vmt_length)
112 		{
113 			struct stat buf;
114 			char *mountp = vmt2dataptr((struct vmount*)vmt, VMT_STUB);
115 			if ((stat (mountp, &buf) != -1) && (buf.st_dev == dev))
116 			{
117 				mytab->mountpoint = mountp;
118 				mytab->mountspecial
119 						= vmt2dataptr((struct vmount*)vmt, VMT_HOSTNAME);
120 				if (mytab->mountspecial.Len())
121 					mytab->mountspecial += ':';
122 				mytab->mountspecial
123 						+= vmt2dataptr((struct vmount*)vmt, VMT_OBJECT);
124 				mytab->mountdevice = dev;
125 				free( buffer );
126 				return sal_True;
127 			}
128 		}
129 	free( buffer );
130 	return sal_False;
131 }
132 
133 #else
134 
135 
GetMountEntry(dev_t dev,struct mymnttab * mytab)136 static sal_Bool GetMountEntry(dev_t dev, struct mymnttab *mytab)
137 {
138 #if defined SOLARIS || defined SINIX
139 	FILE *fp = fopen (MNTTAB, "r");
140 	if (! fp)
141 		return sal_False;
142 	struct mnttab mnt[1];
143 	while (getmntent (fp, mnt) != -1)
144 #elif defined SCO
145 	FILE *fp = fopen (MNTTAB, "r");
146 	if (! fp)
147 		return sal_False;
148 	struct mnttab mnt[1];
149 	while (fread (&mnt, sizeof mnt, 1, fp) > 0)
150 #elif defined DECUNIX || defined AIX
151 	FILE *fp = NULL;
152 	if (! fp)
153 		return sal_False;
154 	struct mnttab mnt[1];
155 	while ( 0 )
156 #else
157 	FILE *fp = setmntent (MOUNTED, "r");
158 	if (! fp)
159 		return sal_False;
160 	struct mnttab *mnt;
161 	while ((mnt = getmntent (fp)) != NULL)
162 #endif
163 	{
164 #ifdef SOLARIS
165 		char *devopt = NULL;
166 		if ( mnt->MOUNTOPTS != NULL )
167 			devopt = strstr (mnt->MOUNTOPTS, "dev=");
168 		if (devopt)
169 		{
170 			if (dev != (dev_t) strtoul (devopt+4, NULL, 16))
171 				continue;
172 		}
173 		else
174 #endif
175 		{
176 			struct stat buf;
177 			if ((stat (mnt->MOUNTPOINT, &buf) == -1) || (buf.st_dev != dev))
178 				continue;
179 		}
180 #		ifdef LINUX
181 		/* #61624# File mit setmntent oeffnen und mit fclose schliessen stoesst
182 		   bei der glibc-2.1 auf wenig Gegenliebe */
183 		endmntent( fp );
184 #		else
185 		fclose (fp);
186 #		endif
187 		mytab->mountspecial = mnt->MOUNTSPECIAL;
188 		mytab->mountpoint 	= mnt->MOUNTPOINT;
189 		mytab->mountdevice 	= dev;
190 #ifndef SCO
191 		mytab->mymnttab_filesystem = mnt->MOUNTFS;
192 #else
193 		mytab->mymnttab_filesystem = "ext2";		//default ist case sensitiv unter unix
194 #endif
195 		return sal_True;
196 	}
197 #	ifdef LINUX
198 	/* #61624# dito */
199 	endmntent( fp );
200 #	else
201 	fclose (fp);
202 #	endif
203 	return sal_False;
204 }
205 
206 #endif
207 
208 /************************************************************************
209 |*
210 |*    DirEntry::IsCaseSensitive()
211 |*
212 |*    Beschreibung
213 |*    Ersterstellung    TPF 25.02.1999
214 |*    Letzte Aenderung  TPF 25.02.1999
215 |*
216 *************************************************************************/
217 
IsCaseSensitive(FSysPathStyle eFormatter) const218 sal_Bool DirEntry::IsCaseSensitive( FSysPathStyle eFormatter ) const
219 {
220 
221 	if (eFormatter==FSYS_STYLE_HOST)
222 	{
223 #ifdef NETBSD
224 		return sal_True;
225 #else
226 		struct stat buf;
227 		DirEntry aPath(*this);
228 		aPath.ToAbs();
229 
230 		while (stat (ByteString(aPath.GetFull(), osl_getThreadTextEncoding()).GetBuffer(), &buf))
231 		{
232 			if (aPath.Level() == 1)
233 			{
234 				return sal_True;	// ich bin unter UNIX, also ist der default im Zweifelsfall case sensitiv
235 			}
236 			aPath = aPath [1];
237 		}
238 
239 		struct mymnttab fsmnt;
240 		GetMountEntry(buf.st_dev, &fsmnt);
241 		if ((fsmnt.mymnttab_filesystem.CompareTo("msdos")==COMPARE_EQUAL) ||
242 		    (fsmnt.mymnttab_filesystem.CompareTo("umsdos")==COMPARE_EQUAL) ||
243 		    (fsmnt.mymnttab_filesystem.CompareTo("vfat")==COMPARE_EQUAL) ||
244 		    (fsmnt.mymnttab_filesystem.CompareTo("hpfs")==COMPARE_EQUAL) ||
245 		    (fsmnt.mymnttab_filesystem.CompareTo("smb")	==COMPARE_EQUAL) ||
246 		    (fsmnt.mymnttab_filesystem.CompareTo("ncpfs")==COMPARE_EQUAL))
247 		{
248 			return sal_False;
249 		}
250 		else
251 		{
252 			return sal_True;
253 		}
254 #endif
255 	}
256 	else
257 	{
258 		sal_Bool isCaseSensitive = sal_True;	// ich bin unter UNIX, also ist der default im Zweifelsfall case sensitiv
259 		switch ( eFormatter )
260 		{
261 			case FSYS_STYLE_MAC:
262 			case FSYS_STYLE_FAT:
263 			case FSYS_STYLE_VFAT:
264 			case FSYS_STYLE_NTFS:
265 			case FSYS_STYLE_NWFS:
266 			case FSYS_STYLE_HPFS:
267 				{
268 					isCaseSensitive = sal_False;
269 					break;
270 				}
271 			case FSYS_STYLE_SYSV:
272 			case FSYS_STYLE_BSD:
273 			case FSYS_STYLE_DETECT:
274 				{
275 					isCaseSensitive = sal_True;
276 					break;
277 				}
278 			default:
279 				{
280 					isCaseSensitive = sal_True;	// ich bin unter UNIX, also ist der default im Zweifelsfall case sensitiv
281 					break;
282 				}
283 		}
284 		return isCaseSensitive;
285 	}
286 }
287 
288 /************************************************************************
289 |*
290 |*    DirEntry::ToAbs()
291 |*
292 |*    Beschreibung      FSYS.SDW
293 |*    Ersterstellung    MI 26.04.91
294 |*    Letzte Aenderung  MA 02.12.91 13:30
295 |*
296 *************************************************************************/
297 
ToAbs()298 sal_Bool DirEntry::ToAbs()
299 {
300 	if ( FSYS_FLAG_VOLUME == eFlag )
301 	{
302 		eFlag = FSYS_FLAG_ABSROOT;
303 		return sal_True;
304 	}
305 
306 	if ( IsAbs() )
307 	  return sal_True;
308 
309 	char sBuf[MAXPATHLEN + 1];
310 	*this = DirEntry( String( getcwd( sBuf, MAXPATHLEN ), osl_getThreadTextEncoding() ) ) + *this;
311 	return IsAbs();
312 }
313 
314 /*************************************************************************
315 |*
316 |*    DirEntry::GetVolume()
317 |*
318 |*    Beschreibung      FSYS.SDW
319 |*    Ersterstellung    MI 04.03.92
320 |*    Letzte Aenderung
321 |*
322 *************************************************************************/
323 
324 namespace { struct mymnt : public rtl::Static< mymnttab, mymnt > {}; }
325 
GetVolume() const326 String DirEntry::GetVolume() const
327 {
328   DBG_CHKTHIS( DirEntry, ImpCheckDirEntry );
329 
330 	DirEntry aPath( *this );
331 	aPath.ToAbs();
332 
333 	struct stat buf;
334 	while (stat (ByteString(aPath.GetFull(), osl_getThreadTextEncoding()).GetBuffer(), &buf))
335 	{
336 		if (aPath.Level() <= 1)
337 			return String();
338 		aPath = aPath [1];
339 	}
340 	mymnttab &rMnt = mymnt::get();
341 	return ((buf.st_dev == rMnt.mountdevice ||
342 				GetMountEntry(buf.st_dev, &rMnt)) ?
343 				    String(rMnt.mountspecial, osl_getThreadTextEncoding()) :
344 					String());
345 }
346 
GetDevice() const347 DirEntry DirEntry::GetDevice() const
348 {
349   DBG_CHKTHIS( DirEntry, ImpCheckDirEntry );
350 
351 	DirEntry aPath( *this );
352 	aPath.ToAbs();
353 
354 	struct stat buf;
355 	while (stat (ByteString(aPath.GetFull(), osl_getThreadTextEncoding()).GetBuffer(), &buf))
356 	{
357 		if (aPath.Level() <= 1)
358 			return String();
359 		aPath = aPath [1];
360 	}
361 	mymnttab &rMnt = mymnt::get();
362 	return ((buf.st_dev == rMnt.mountdevice ||
363 				GetMountEntry(buf.st_dev, &rMnt)) ?
364 				    String( rMnt.mountpoint, osl_getThreadTextEncoding()) :
365 					String());
366 }
367 
368 /*************************************************************************
369 |*
370 |*    DirEntry::SetCWD()
371 |*
372 |*    Beschreibung      FSYS.SDW
373 |*    Ersterstellung    MI 26.04.91
374 |*    Letzte Aenderung  DV 04.11.92
375 |*
376 *************************************************************************/
377 
SetCWD(sal_Bool bSloppy) const378 sal_Bool DirEntry::SetCWD( sal_Bool bSloppy ) const
379 {
380     DBG_CHKTHIS( DirEntry, ImpCheckDirEntry );
381 
382 
383 	ByteString aPath( GetFull(), osl_getThreadTextEncoding());
384 	if ( !chdir( aPath.GetBuffer() ) )
385 	{
386 		return sal_True;
387 	}
388 	else
389 	{
390 		if ( bSloppy && !chdir(aPath.GetBuffer()) )
391 		{
392 			return sal_True;
393 		}
394 		else
395 		{
396 			return sal_False;
397 		}
398 	}
399 }
400 
401 //-------------------------------------------------------------------------
402 
Init()403 sal_uInt16 DirReader_Impl::Init()
404 {
405 	return 0;
406 }
407 
408 //-------------------------------------------------------------------------
409 
Read()410 sal_uInt16 DirReader_Impl::Read()
411 {
412 	if (!pDosDir)
413 	{
414 		pDosDir = opendir( (char*) ByteString(aPath, osl_getThreadTextEncoding()).GetBuffer() );
415 	}
416 
417 	if (!pDosDir)
418 	{
419 		bReady = sal_True;
420 		return 0;
421 	}
422 
423     // Directories und Files auflisten?
424 	if ( ( pDir->eAttrMask & FSYS_KIND_DIR || pDir->eAttrMask & FSYS_KIND_FILE ) &&
425 		 ( ( pDosEntry = readdir( pDosDir ) ) != NULL ) )
426 	{
427 	String aD_Name(pDosEntry->d_name, osl_getThreadTextEncoding());
428         if ( pDir->aNameMask.Matches( aD_Name  ) )
429         {
430 			DirEntryFlag eFlag =
431 					0 == strcmp( pDosEntry->d_name, "." ) ? FSYS_FLAG_CURRENT
432 				:	0 == strcmp( pDosEntry->d_name, ".." ) ? FSYS_FLAG_PARENT
433 				:	FSYS_FLAG_NORMAL;
434             DirEntry *pTemp = new DirEntry( ByteString(pDosEntry->d_name), eFlag, FSYS_STYLE_UNX );
435             if ( pParent )
436                 pTemp->ImpChangeParent( new DirEntry( *pParent ), sal_False);
437             FileStat aStat( *pTemp );
438             if ( ( ( ( pDir->eAttrMask & FSYS_KIND_DIR ) &&
439 					 ( aStat.IsKind( FSYS_KIND_DIR ) ) ) ||
440 				   ( ( pDir->eAttrMask & FSYS_KIND_FILE ) &&
441 					 !( aStat.IsKind( FSYS_KIND_DIR ) ) ) ) &&
442 				 !( pDir->eAttrMask & FSYS_KIND_VISIBLE &&
443 					pDosEntry->d_name[0] == '.' ) )
444             {
445                 if ( pDir->pStatLst ) //Status fuer Sort gewuenscht?
446                     pDir->ImpSortedInsert( pTemp, new FileStat( aStat ) );
447                 else
448                     pDir->ImpSortedInsert( pTemp, NULL );;
449 				return 1;
450             }
451             else
452                 delete pTemp;
453         }
454 	}
455 	else
456 		bReady = sal_True;
457 	return 0;
458 }
459 
460 /*************************************************************************
461 |*
462 |*    FileStat::FileStat()
463 |*
464 |*    Beschreibung      FSYS.SDW
465 |*    Ersterstellung    MA 05.11.91
466 |*    Letzte Aenderung  MA 07.11.91
467 |*
468 *************************************************************************/
469 
FileStat(const void *,const void *)470 FileStat::FileStat( const void *, const void * ):
471 	aDateCreated(0),
472 	aTimeCreated(0),
473 	aDateModified(0),
474 	aTimeModified(0),
475 	aDateAccessed(0),
476 	aTimeAccessed(0)
477 {
478 }
479 
480 /*************************************************************************
481 |*
482 |*    FileStat::Update()
483 |*
484 |*    Beschreibung      FSYS.SDW
485 |*    Ersterstellung    MI 11.06.91
486 |*    Letzte Aenderung  MA 07.11.91
487 |*
488 *************************************************************************/
Update(const DirEntry & rDirEntry,sal_Bool)489 sal_Bool FileStat::Update( const DirEntry& rDirEntry, sal_Bool )
490 {
491 
492 	nSize = 0;
493 	nKindFlags = 0;
494 	aCreator.Erase();
495 	aType.Erase();
496 	aDateCreated = Date(0);
497 	aTimeCreated = Time(0);
498 	aDateModified = Date(0);
499 	aTimeModified = Time(0);
500 	aDateAccessed = Date(0);
501 	aTimeAccessed = Time(0);
502 
503 	if ( !rDirEntry.IsValid() )
504 	{
505 		nError = FSYS_ERR_NOTEXISTS;
506 		return sal_False;
507 	}
508 
509 	// Sonderbehandlung falls es sich um eine Root handelt
510 	if ( rDirEntry.eFlag == FSYS_FLAG_ABSROOT )
511 	{
512 		nKindFlags = FSYS_KIND_DIR;
513 		nError = FSYS_ERR_OK;
514 		return sal_True;
515 	}
516 
517 	struct stat aStat;
518 	ByteString aPath( rDirEntry.GetFull(), osl_getThreadTextEncoding() );
519 	if ( stat( (char*) aPath.GetBuffer(), &aStat ) )
520 	{
521 		// pl: #67851#
522 		// do this here, because an existing filename containing "wildcards"
523 		// should be handled as a file, not a wildcard
524 		// note that this is not a solution, since filenames containing special characters
525 		// are handled badly across the whole Office
526 
527 		// Sonderbehandlung falls es sich um eine Wildcard handelt
528 		ByteString aTempName( rDirEntry.GetName(), osl_getThreadTextEncoding() );
529 		if ( strchr( (char*) aTempName.GetBuffer(), '?' ) ||
530 			 strchr( (char*) aTempName.GetBuffer(), '*' ) ||
531 			 strchr( (char*) aTempName.GetBuffer(), ';' ) )
532 		{
533 			nKindFlags = FSYS_KIND_WILD;
534 			nError = FSYS_ERR_OK;
535 			return sal_True;
536 		}
537 
538 		nError = FSYS_ERR_NOTEXISTS;
539 		return sal_False;
540 	}
541 
542 	nError = FSYS_ERR_OK;
543 	nSize = aStat.st_size;
544 
545 	nKindFlags = FSYS_KIND_UNKNOWN;
546 	if ( ( aStat.st_mode & S_IFDIR ) == S_IFDIR )
547 		nKindFlags = nKindFlags | FSYS_KIND_DIR;
548 	if ( ( aStat.st_mode & S_IFREG ) == S_IFREG )
549 		nKindFlags = nKindFlags | FSYS_KIND_FILE;
550 	if ( ( aStat.st_mode & S_IFCHR ) == S_IFCHR )
551 		nKindFlags = nKindFlags | FSYS_KIND_DEV | FSYS_KIND_CHAR;
552 	if ( ( aStat.st_mode & S_IFBLK ) == S_IFBLK )
553 		nKindFlags = nKindFlags | FSYS_KIND_DEV | FSYS_KIND_BLOCK;
554 	if ( nKindFlags == FSYS_KIND_UNKNOWN )
555 		nKindFlags = nKindFlags | FSYS_KIND_FILE;
556 
557 	Unx2DateAndTime( aStat.st_ctime, aTimeCreated, aDateCreated );
558 	Unx2DateAndTime( aStat.st_mtime, aTimeModified, aDateModified );
559 	Unx2DateAndTime( aStat.st_atime, aTimeAccessed, aDateAccessed );
560 
561 	return sal_True;
562 }
563 
564 //====================================================================
565 
TempDirImpl(char * pBuf)566 const char *TempDirImpl( char *pBuf )
567 {
568 #ifdef MACOSX
569     // P_tmpdir is /var/tmp on Mac OS X, and it is not cleaned up on system
570     // startup
571     strcpy( pBuf, "/tmp" );
572 #else
573     const char *pValue = getenv( "TEMP" );
574     if ( !pValue )
575         pValue = getenv( "TMP" );
576     if ( pValue )
577         strcpy( pBuf, pValue );
578     else
579 		// auf Solaris und Linux ist P_tmpdir vorgesehen
580         strcpy( pBuf, P_tmpdir );
581 		// hart auf "/tmp"  sollte wohl nur im Notfall verwendet werden
582         //strcpy( pBuf, "/tmp" );
583 #endif /* MACOSX */
584 
585     return pBuf;
586 }
587 
588 /*************************************************************************
589 |*
590 |*    DirEntry::GetPathStyle() const
591 |*
592 |*    Beschreibung
593 |*    Ersterstellung    MI 11.05.95
594 |*    Letzte Aenderung  MI 11.05.95
595 |*
596 *************************************************************************/
597 
GetPathStyle(const String &)598 FSysPathStyle DirEntry::GetPathStyle( const String & )
599 {
600     return FSYS_STYLE_UNX;
601 }
602 
603 /*************************************************************************
604 |*
605 |*    FileStat::SetDateTime
606 |*
607 |*    Ersterstellung	PB  27.06.97
608 |*    Letzte Aenderung
609 |*
610 *************************************************************************/
611 
SetDateTime(const String & rFileName,const DateTime & rNewDateTime)612 void FileStat::SetDateTime( const String& rFileName,
613 			    const DateTime& rNewDateTime )
614 {
615 	tm times;
616 
617 	times.tm_year = rNewDateTime.GetYear()  - 1900;  	// 1997 -> 97
618 	times.tm_mon  = rNewDateTime.GetMonth() - 1;		// 0 == Januar!
619 	times.tm_mday = rNewDateTime.GetDay();
620 
621 	times.tm_hour = rNewDateTime.GetHour();
622 	times.tm_min  = rNewDateTime.GetMin();
623 	times.tm_sec  = rNewDateTime.GetSec();
624 
625 	times.tm_wday  = 0;
626 	times.tm_yday  = 0;
627 #ifdef SOLARIS
628 	times.tm_isdst = -1;
629 #else
630 	times.tm_isdst = 0;
631 #endif
632 
633 	time_t time = mktime (&times);
634 
635 	if (time != (time_t) -1)
636 	{
637 		struct utimbuf u_time;
638 		u_time.actime = time;
639 		u_time.modtime = time;
640 		utime (ByteString(rFileName, osl_getThreadTextEncoding()).GetBuffer(), &u_time);
641 	}
642 }
643 
644 //=========================================================================
645 
QueryDiskSpace(const String &,BigInt &,BigInt &)646 ErrCode FileStat::QueryDiskSpace( const String &, BigInt &, BigInt & )
647 {
648 	return ERRCODE_IO_NOTSUPPORTED;
649 }
650 
651 //=========================================================================
652 
FSysEnableSysErrorBox(sal_Bool)653 void FSysEnableSysErrorBox( sal_Bool )
654 {
655 }
656 
657