Imported Upstream version 2.17
[debian/cpmtools] / cpmfs.c
1 /* #includes */ /*{{{C}}}*//*{{{*/
2 #include "config.h"
3
4 #include <sys/stat.h>
5 #include <assert.h>
6 #include <ctype.h>
7 #include <errno.h>
8 #include <stdlib.h>
9 #include <stdio.h>
10 #include <string.h>
11 #include <time.h>
12
13 #include "cpmdir.h"
14 #include "cpmfs.h"
15
16 #ifdef USE_DMALLOC
17 #include <dmalloc.h>
18 #endif
19 /*}}}*/
20 /* #defines */ /*{{{*/
21 #undef CPMFS_DEBUG
22
23 /* Number of _used_ bits per int */
24
25 #define INTBITS ((int)(sizeof(int)*8))
26
27 /* Convert BCD datestamp digits to binary */
28
29 #define BCD2BIN(x) ((((x)>>4)&0xf)*10 + ((x)&0xf))
30
31 #define BIN2BCD(x) (((((x)/10)&0xf)<<4) + (((x)%10)&0xf))
32
33 /* There are four reserved directory entries: ., .., [passwd] and [label].
34 The first two of them refer to the same inode. */
35
36 #define RESERVED_ENTRIES 4
37
38 /* CP/M does not support any kind of inodes, so they are simulated.
39 Inode 0-(maxdir-1) correlate to the lowest extent number (not the first
40 extent of the file in the directory) of a file.  Inode maxdir is the
41 root directory, inode maxdir+1 is the optional passwd file and inode
42 maxdir+2 the optional disk label. */
43
44 #define RESERVED_INODES 3
45
46 #define PASSWD_RECLEN 24
47 /*}}}*/
48
49 extern char **environ;
50 const char *boo;
51 static mode_t s_ifdir=1;
52 static mode_t s_ifreg=1;
53
54 /* memcpy7            -- Copy string, leaving 8th bit alone      */ /*{{{*/
55 static void memcpy7(char *dest, const char *src, int count)
56 {
57   while (count--)
58   {
59     *dest = ((*dest) & 0x80) | ((*src) & 0x7F);
60     ++dest;
61     ++src;
62   }
63 }
64 /*}}}*/
65
66 /* file name conversions */ 
67 /* splitFilename      -- split file name into name and extension */ /*{{{*/
68 static int splitFilename(const char *fullname, int type, char *name, char *ext, int *user) 
69 {
70   int i,j;
71
72   assert(fullname!=(const char*)0);
73   assert(name!=(char*)0);
74   assert(ext!=(char*)0);
75   assert(user!=(int*)0);
76   memset(name,' ',8);
77   memset(ext,' ',3);
78   if (!isdigit(fullname[0]) || !isdigit(fullname[1]))
79   {
80     boo="illegal CP/M filename";
81     return -1;
82   }
83   *user=10*(fullname[0]-'0')+(fullname[1]-'0');
84   fullname+=2;
85   if ((fullname[0]=='\0') || *user>=((type&CPMFS_HI_USER) ? 32 : 16))
86   {
87     boo="illegal CP/M filename";
88     return -1;
89   }
90   for (i=0; i<8 && fullname[i] && fullname[i]!='.'; ++i) if (!ISFILECHAR(i,fullname[i]))
91   {
92     boo="illegal CP/M filename";
93     return -1;
94   }
95   else name[i]=toupper(fullname[i]);
96   if (fullname[i]=='.')
97   {
98     ++i;
99     for (j=0; j<3 && fullname[i]; ++i,++j) if (!ISFILECHAR(1,fullname[i]))
100     {
101       boo="illegal CP/M filename";
102       return -1;
103     }
104     else ext[j]=toupper(fullname[i]);
105     if (i==1 && j==0)
106     {
107       boo="illegal CP/M filename";
108       return -1;
109     }
110   }
111   return 0;
112 }
113 /*}}}*/
114 /* isMatching         -- do two file names match?                */ /*{{{*/
115 static int isMatching(int user1, const char *name1, const char *ext1, int user2, const char *name2, const char *ext2)
116 {
117   int i;
118
119   assert(name1!=(const char*)0);
120   assert(ext1!=(const char*)0);
121   assert(name2!=(const char*)0);
122   assert(ext2!=(const char*)0);
123   if (user1!=user2) return 0;
124   for (i=0; i<8; ++i) if ((name1[i]&0x7f)!=(name2[i]&0x7f)) return 0;
125   for (i=0; i<3; ++i) if ((ext1[i]&0x7f)!=(ext2[i]&0x7f)) return 0;
126   return 1;
127 }
128 /*}}}*/
129
130 /* time conversions */
131 /* cpm2unix_time      -- convert CP/M time to UTC                */ /*{{{*/
132 static time_t cpm2unix_time(int days, int hour, int min)
133 {
134   /* CP/M stores timestamps in local time.  We don't know which     */
135   /* timezone was used and if DST was in effect.  Assuming it was   */
136   /* the current offset from UTC is most sensible, but not perfect. */
137
138   int year,days_per_year;
139   static int days_per_month[]={31,0,31,30,31,30,31,31,30,31,30,31};
140   char **old_environ;
141   static char gmt0[]="TZ=GMT0";
142   static char *gmt_env[]={ gmt0, (char*)0 };
143   struct tm tms;
144   time_t lt,t;
145
146   time(&lt);
147   t=lt;
148   tms=*localtime(&lt);
149   old_environ=environ;
150   environ=gmt_env;
151   lt=mktime(&tms);
152   lt-=t;
153   tms.tm_sec=0;
154   tms.tm_min=((min>>4)&0xf)*10+(min&0xf);
155   tms.tm_hour=((hour>>4)&0xf)*10+(hour&0xf);
156   tms.tm_mday=1;
157   tms.tm_mon=0;
158   tms.tm_year=78;
159   tms.tm_isdst=-1;
160   for (;;)
161   {
162     year=tms.tm_year+1900;
163     days_per_year=((year%4)==0 && ((year%100) || (year%400)==0)) ? 366 : 365;
164     if (days>days_per_year)
165     {
166       days-=days_per_year;
167       ++tms.tm_year;
168     }
169     else break;
170   }
171   for (;;)
172   {
173     days_per_month[1]=(days_per_year==366) ? 29 : 28;
174     if (days>days_per_month[tms.tm_mon])
175     {
176       days-=days_per_month[tms.tm_mon];
177       ++tms.tm_mon;
178     }
179     else break;
180   }
181   t=mktime(&tms)+(days-1)*24*3600;
182   environ=old_environ;
183   t-=lt;
184   return t;
185 }
186 /*}}}*/
187 /* unix2cpm_time      -- convert UTC to CP/M time                */ /*{{{*/
188 static void unix2cpm_time(time_t now, int *days, int *hour, int *min) 
189 {
190   struct tm *tms;
191   int i;
192
193   tms=localtime(&now);
194   *min=((tms->tm_min/10)<<4)|(tms->tm_min%10);
195   *hour=((tms->tm_hour/10)<<4)|(tms->tm_hour%10);
196   for (i=1978,*days=0; i<1900+tms->tm_year; ++i)
197   {
198     *days+=365;
199     if (i%4==0 && (i%100!=0 || i%400==0)) ++*days;
200   }
201   *days += tms->tm_yday+1;
202 }
203 /*}}}*/
204 /* ds2unix_time       -- convert DS to Unix time                 */ /*{{{*/
205 static time_t ds2unix_time(const struct dsEntry *entry) 
206 {
207   struct tm tms;
208   int yr;
209
210   if (entry->minute==0 &&
211       entry->hour==0 &&
212       entry->day==0 &&
213       entry->month==0 &&
214       entry->year==0) return 0;
215   
216   tms.tm_isdst = -1;
217   tms.tm_sec = 0;
218   tms.tm_min = BCD2BIN( entry->minute );
219   tms.tm_hour = BCD2BIN( entry->hour );
220   tms.tm_mday = BCD2BIN( entry->day );
221   tms.tm_mon = BCD2BIN( entry->month ) - 1;
222   
223   yr = BCD2BIN(entry->year);
224   if (yr<70) yr+=100;
225   tms.tm_year = yr;
226   
227   return mktime(&tms);
228 }
229 /*}}}*/
230 /* unix2ds_time       -- convert Unix to DS time                 */ /*{{{*/
231 static void unix2ds_time(time_t now, struct dsEntry *entry) 
232 {
233   struct tm *tms;
234   int yr;
235
236   if ( now==0 )
237   {
238     entry->minute=entry->hour=entry->day=entry->month=entry->year = 0;
239   }
240   else
241   {
242     tms=localtime(&now);
243     entry->minute = BIN2BCD( tms->tm_min );
244     entry->hour = BIN2BCD( tms->tm_hour );
245     entry->day = BIN2BCD( tms->tm_mday );
246     entry->month = BIN2BCD( tms->tm_mon + 1 );
247     
248     yr = tms->tm_year;
249     if ( yr>100 ) yr -= 100;
250     entry->year = BIN2BCD( yr );
251   }
252 }
253 /*}}}*/
254
255 /* allocation vector bitmap functions */
256 /* alvInit            -- init allocation vector                  */ /*{{{*/
257 static void alvInit(const struct cpmSuperBlock *d)
258 {
259   int i,j,offset,block;
260
261   assert(d!=(const struct cpmSuperBlock*)0);
262   /* clean bitmap */ /*{{{*/
263   memset(d->alv,0,d->alvSize*sizeof(int));
264   /*}}}*/
265   /* mark directory blocks as used */ /*{{{*/
266   *d->alv=(1<<((d->maxdir*32+d->blksiz-1)/d->blksiz))-1;
267   /*}}}*/
268   for (i=0; i<d->maxdir; ++i) /* mark file blocks as used */ /*{{{*/
269   {
270     if (d->dir[i].status>=0 && d->dir[i].status<=(d->type&CPMFS_HI_USER ? 31 : 15))
271     {
272 #ifdef CPMFS_DEBUG
273       fprintf(stderr,"alvInit: allocate extent %d\n",i);
274 #endif
275       for (j=0; j<16; ++j)
276       {
277         block=(unsigned char)d->dir[i].pointers[j];
278         if (d->size>=256) block+=(((unsigned char)d->dir[i].pointers[++j])<<8);
279         if (block && block<d->size)
280         {
281 #ifdef CPMFS_DEBUG
282           fprintf(stderr,"alvInit: allocate block %d\n",block);
283 #endif
284           offset=block/INTBITS;
285           d->alv[offset]|=(1<<block%INTBITS);
286         }
287       }
288     }
289   }
290   /*}}}*/
291 }
292 /*}}}*/
293 /* allocBlock         -- allocate a new disk block               */ /*{{{*/
294 static int allocBlock(const struct cpmSuperBlock *drive)
295 {
296   int i,j,bits,block;
297
298   assert(drive!=(const struct cpmSuperBlock*)0);
299   for (i=0; i<drive->alvSize; ++i)
300   {
301     for (j=0,bits=drive->alv[i]; j<INTBITS; ++j)
302     {
303       if ((bits&1)==0)
304       {
305         block=i*INTBITS+j;
306         if (block>=drive->size)
307         {
308           boo="device full";
309           return -1;
310         }
311         drive->alv[i] |= (1<<j);
312         return block;
313       }
314       bits >>= 1;
315     }
316   }
317   boo="device full";
318   return -1;
319 }
320 /*}}}*/
321
322 /* logical block I/O */
323 /* readBlock          -- read a (partial) block                  */ /*{{{*/
324 static int readBlock(const struct cpmSuperBlock *d, int blockno, char *buffer, int start, int end)
325 {
326   int sect, track, counter;
327
328   assert(blockno>=0);
329   assert(blockno<d->size);
330   assert(buffer!=(char*)0);
331   if (end<0) end=d->blksiz/d->secLength-1;
332   sect=(blockno*(d->blksiz/d->secLength)+ d->sectrk*d->boottrk)%d->sectrk;
333   track=(blockno*(d->blksiz/d->secLength)+ d->sectrk*d->boottrk)/d->sectrk;
334   for (counter=0; counter<=end; ++counter)
335   {
336     const char *err;
337
338     if (counter>=start && (err=Device_readSector(&d->dev,track,d->skewtab[sect],buffer+(d->secLength*counter))))
339     {
340       boo=err;
341       return -1;
342     }
343     ++sect;
344     if (sect>=d->sectrk) 
345     {
346       sect = 0;
347       ++track;
348     }
349   }
350   return 0;
351 }
352 /*}}}*/
353 /* writeBlock         -- write a (partial) block                 */ /*{{{*/
354 static int writeBlock(const struct cpmSuperBlock *d, int blockno, const char *buffer, int start, int end)
355 {
356   int sect, track, counter;
357
358   assert(blockno>=0);
359   assert(blockno<d->size);
360   assert(buffer!=(const char*)0);
361   if (end < 0) end=d->blksiz/d->secLength-1;
362   sect = (blockno*(d->blksiz/d->secLength))%d->sectrk;
363   track = (blockno*(d->blksiz/d->secLength))/d->sectrk+d->boottrk;
364   for (counter = 0; counter<=end; ++counter)
365   {
366     const char *err;
367
368     if (counter>=start && (err=Device_writeSector(&d->dev,track,d->skewtab[sect],buffer+(d->secLength*counter))))
369     {
370       boo=err;
371       return -1;
372     }
373     ++sect;
374     if (sect>=d->sectrk) 
375     {
376       sect=0;
377       ++track;
378     }
379   }
380   return 0;
381 }
382 /*}}}*/
383
384 /* directory management */
385 /* findFileExtent     -- find first/next extent for a file       */ /*{{{*/
386 static int findFileExtent(const struct cpmSuperBlock *sb, int user, const char *name, const char *ext, int start, int extno)
387 {
388   boo="file already exists";
389   for (; start<sb->maxdir; ++start)
390   {
391     if
392     (
393       ((unsigned char)sb->dir[start].status)<=(sb->type&CPMFS_HI_USER ? 31 : 15)
394       && (extno==-1 || (EXTENT(sb->dir[start].extnol,sb->dir[start].extnoh)/sb->extents)==(extno/sb->extents))
395       && isMatching(user,name,ext,sb->dir[start].status,sb->dir[start].name,sb->dir[start].ext)
396     ) return start;
397   }
398   boo="file not found";
399   return -1;
400 }
401 /*}}}*/
402 /* findFreeExtent     -- find first free extent                  */ /*{{{*/
403 static int findFreeExtent(const struct cpmSuperBlock *drive)
404 {
405   int i;
406
407   for (i=0; i<drive->maxdir; ++i) if (drive->dir[i].status==(char)0xe5) return (i);
408   boo="directory full";
409   return -1;
410 }
411 /*}}}*/
412 /* updateTimeStamps   -- convert time stamps to CP/M format      */ /*{{{*/
413 static void updateTimeStamps(const struct cpmInode *ino, int extent)
414 {
415   struct PhysDirectoryEntry *date;
416   int i;
417   int ca_min,ca_hour,ca_days,u_min,u_hour,u_days;
418
419   if (!S_ISREG(ino->mode)) return;
420 #ifdef CPMFS_DEBUG
421   fprintf(stderr,"CPMFS: updating time stamps for inode %d (%d)\n",extent,extent&3);
422 #endif
423   unix2cpm_time(ino->sb->cnotatime ? ino->ctime : ino->atime,&ca_days,&ca_hour,&ca_min);
424   unix2cpm_time(ino->mtime,&u_days,&u_hour,&u_min);
425   if ((ino->sb->type&CPMFS_CPM3_DATES) && (date=ino->sb->dir+(extent|3))->status==0x21)
426   {
427     ino->sb->dirtyDirectory=1;
428     switch (extent&3)
429     {
430       case 0: /* first entry */ /*{{{*/
431       {
432         date->name[0]=ca_days&0xff; date->name[1]=ca_days>>8;
433         date->name[2]=ca_hour;
434         date->name[3]=ca_min;
435         date->name[4]=u_days&0xff; date->name[5]=u_days>>8;
436         date->name[6]=u_hour;
437         date->name[7]=u_min;
438         break;
439       }
440       /*}}}*/
441       case 1: /* second entry */ /*{{{*/
442       {
443         date->ext[2]=ca_days&0xff; date->extnol=ca_days>>8;
444         date->lrc=ca_hour;
445         date->extnoh=ca_min;
446         date->blkcnt=u_days&0xff; date->pointers[0]=u_days>>8;
447         date->pointers[1]=u_hour;
448         date->pointers[2]=u_min;
449         break;
450       }
451       /*}}}*/
452       case 2: /* third entry */ /*{{{*/
453       {
454         date->pointers[5]=ca_days&0xff; date->pointers[6]=ca_days>>8;
455         date->pointers[7]=ca_hour;
456         date->pointers[8]=ca_min;
457         date->pointers[9]=u_days&0xff; date->pointers[10]=u_days>>8;
458         date->pointers[11]=u_hour;
459         date->pointers[12]=u_min;
460         break;
461       }
462       /*}}}*/
463     }
464   }
465 }
466 /*}}}*/
467 /* updateDsStamps     -- set time in datestamper file            */ /*{{{*/
468 static void updateDsStamps(const struct cpmInode *ino, int extent)
469 {
470   int yr;
471   struct tm *cpm_time;
472   struct dsDate *stamp;
473   
474   if (!S_ISREG(ino->mode)) return;
475   if ( !(ino->sb->type&CPMFS_DS_DATES) ) return;
476
477 #ifdef CPMFS_DEBUG
478   fprintf(stderr,"CPMFS: updating ds stamps for inode %d (%d)\n",extent,extent&3);
479 #endif
480   
481   /* Get datestamp struct */
482   stamp = ino->sb->ds+extent;
483   
484   unix2ds_time( ino->mtime, &stamp->modify );
485   unix2ds_time( ino->ctime, &stamp->create );
486   unix2ds_time( ino->atime, &stamp->access );
487
488   ino->sb->dirtyDs = 1;
489 }
490 /*}}}*/
491 /* readTimeStamps     -- read CP/M time stamp                    */ /*{{{*/
492 static int readTimeStamps(struct cpmInode *i, int lowestExt) 
493 {
494   /* variables */ /*{{{*/
495   struct PhysDirectoryEntry *date;
496   int u_days=0,u_hour=0,u_min=0;
497   int ca_days=0,ca_hour=0,ca_min=0;
498   int protectMode=0;
499   /*}}}*/
500   
501   if ( (i->sb->type&CPMFS_CPM3_DATES) && (date=i->sb->dir+(lowestExt|3))->status==0x21 )
502   {
503     switch (lowestExt&3)
504     {
505       case 0: /* first entry of the four */ /*{{{*/
506       {
507         ca_days=((unsigned char)date->name[0])+(((unsigned char)date->name[1])<<8);
508         ca_hour=(unsigned char)date->name[2];
509         ca_min=(unsigned char)date->name[3];
510         u_days=((unsigned char)date->name[4])+(((unsigned char)date->name[5])<<8);
511         u_hour=(unsigned char)date->name[6];
512         u_min=(unsigned char)date->name[7];
513         protectMode=(unsigned char)date->ext[0];
514         break;
515       }
516       /*}}}*/
517       case 1: /* second entry */ /*{{{*/
518       {
519         ca_days=((unsigned char)date->ext[2])+(((unsigned char)date->extnol)<<8);
520         ca_hour=(unsigned char)date->lrc;
521         ca_min=(unsigned char)date->extnoh;
522         u_days=((unsigned char)date->blkcnt)+(((unsigned char)date->pointers[0])<<8);
523         u_hour=(unsigned char)date->pointers[1];
524         u_min=(unsigned char)date->pointers[2];
525         protectMode=(unsigned char)date->pointers[3];
526         break;
527       }
528       /*}}}*/
529       case 2: /* third one */ /*{{{*/
530       {
531         ca_days=((unsigned char)date->pointers[5])+(((unsigned char)date->pointers[6])<<8);
532         ca_hour=(unsigned char)date->pointers[7];
533         ca_min=(unsigned char)date->pointers[8];
534         u_days=((unsigned char)date->pointers[9])+(((unsigned char)date->pointers[10])<<8);
535         u_hour=(unsigned char)date->pointers[11];
536         u_min=(unsigned char)date->pointers[12];
537         protectMode=(unsigned char)date->pointers[13];
538         break;
539       }
540       /*}}}*/
541     }
542     if (i->sb->cnotatime)
543     {
544       i->ctime=cpm2unix_time(ca_days,ca_hour,ca_min);
545       i->atime=0;
546     }
547     else
548     {
549       i->ctime=0;
550       i->atime=cpm2unix_time(ca_days,ca_hour,ca_min);
551     }
552     i->mtime=cpm2unix_time(u_days,u_hour,u_min);
553   }
554   else
555   {
556     i->atime=i->mtime=i->ctime=0;
557     protectMode=0;
558   }
559   
560   return protectMode;
561 }
562 /*}}}*/
563 /* readDsStamps       -- read datestamper time stamp             */ /*{{{*/
564 static void readDsStamps(struct cpmInode *i, int lowestExt) 
565 {
566   struct dsDate *stamp;
567   
568   if ( !(i->sb->type&CPMFS_DS_DATES) ) return;
569
570   /* Get datestamp */
571   stamp = i->sb->ds+lowestExt;
572   
573   i->mtime = ds2unix_time(&stamp->modify);
574   i->ctime = ds2unix_time(&stamp->create);
575   i->atime = ds2unix_time(&stamp->access);
576 }
577 /*}}}*/
578
579 /* match              -- match filename against a pattern        */ /*{{{*/
580 static int recmatch(const char *a, const char *pattern)
581 {
582   int first=1;
583
584   while (*pattern)
585   {
586     switch (*pattern)
587     {
588       case '*':
589       {
590         if (*a=='.' && first) return 1;
591         ++pattern;
592         while (*a) if (recmatch(a,pattern)) return 1; else ++a;
593         break;
594       }
595       case '?':
596       {
597         if (*a) { ++a; ++pattern; } else return 0;
598         break;
599       }
600       default: if (tolower(*a)==tolower(*pattern)) { ++a; ++pattern; } else return 0;
601     }
602     first=0;
603   }
604   return (*pattern=='\0' && *a=='\0');
605 }
606
607 int match(const char *a, const char *pattern) 
608 {
609   int user;
610   char pat[255];
611
612   assert(strlen(pattern)<255);
613   if (isdigit(*pattern) && *(pattern+1)==':') { user=(*pattern-'0'); pattern+=2; }
614   else if (isdigit(*pattern) && isdigit(*(pattern+1)) && *(pattern+2)==':') { user=(10*(*pattern-'0')+(*(pattern+1)-'0')); pattern+=3; }
615   else user=-1;
616   if (user==-1) sprintf(pat,"??%s",pattern);
617   else sprintf(pat,"%02d%s",user,pattern);
618   return recmatch(a,pat);
619 }
620
621 /*}}}*/
622 /* cpmglob            -- expand CP/M style wildcards             */ /*{{{*/
623 void cpmglob(int optin, int argc, char * const argv[], struct cpmInode *root, int *gargc, char ***gargv)
624 {
625   struct cpmFile dir;
626   int entries,dirsize=0;
627   struct cpmDirent *dirent=(struct cpmDirent*)0;
628   int gargcap=0,i,j;
629
630   *gargv=(char**)0;
631   *gargc=0;
632   cpmOpendir(root,&dir);
633   entries=0;
634   dirsize=8;
635   dirent=malloc(sizeof(struct cpmDirent)*dirsize);
636   while (cpmReaddir(&dir,&dirent[entries]))
637   {
638     ++entries;
639     if (entries==dirsize) dirent=realloc(dirent,sizeof(struct cpmDirent)*(dirsize*=2));
640   }
641   for (i=optin; i<argc; ++i)
642   {
643     int found;
644
645     for (j=0,found=0; j<entries; ++j)
646     {
647       if (match(dirent[j].name,argv[i]))
648       {
649         if (*gargc==gargcap) *gargv=realloc(*gargv,sizeof(char*)*(gargcap ? (gargcap*=2) : (gargcap=16)));
650         (*gargv)[*gargc]=strcpy(malloc(strlen(dirent[j].name)+1),dirent[j].name);
651         ++*gargc;
652         ++found;
653       }
654     }
655   }
656   free(dirent);
657 }
658 /*}}}*/
659
660 /* superblock management */
661 /* diskdefReadSuper   -- read super block from diskdefs file     */ /*{{{*/
662 static int diskdefReadSuper(struct cpmSuperBlock *d, const char *format)
663 {
664   char line[256];
665   FILE *fp;
666   int insideDef=0,found=0;
667
668   d->type=0;
669   if ((fp=fopen("diskdefs","r"))==(FILE*)0 && (fp=fopen(DISKDEFS,"r"))==(FILE*)0)
670   {
671     fprintf(stderr,"%s: Neither `diskdefs' nor `" DISKDEFS "' could be opened.\n",cmd);
672     exit(1);
673   }
674   while (fgets(line,sizeof(line),fp)!=(char*)0)
675   {
676     int argc;
677     char *argv[2];
678
679     for (argc=0; argc<1 && (argv[argc]=strtok(argc ? (char*)0 : line," \t\n")); ++argc);
680     if ((argv[argc]=strtok((char*)0,"\n"))!=(char*)0) ++argc;
681     if (insideDef)
682     {
683       if (argc==1 && strcmp(argv[0],"end")==0)
684       {
685         insideDef=0;
686         d->size=(d->secLength*d->sectrk*(d->tracks-d->boottrk))/d->blksiz;
687         if (d->extents==0) d->extents=((d->size>=256 ? 8 : 16)*d->blksiz)/16384;
688         if (d->extents==0) d->extents=1;
689         if (found) break;
690       }
691       else if (argc==2)
692       {
693         if (strcmp(argv[0],"seclen")==0) d->secLength=strtol(argv[1],(char**)0,0);
694         else if (strcmp(argv[0],"tracks")==0) d->tracks=strtol(argv[1],(char**)0,0);
695         else if (strcmp(argv[0],"sectrk")==0) d->sectrk=strtol(argv[1],(char**)0,0);
696         else if (strcmp(argv[0],"blocksize")==0) d->blksiz=strtol(argv[1],(char**)0,0);
697         else if (strcmp(argv[0],"maxdir")==0) d->maxdir=strtol(argv[1],(char**)0,0);
698         else if (strcmp(argv[0],"skew")==0) d->skew=strtol(argv[1],(char**)0,0);
699         else if (strcmp(argv[0],"skewtab")==0)
700         {
701           int pass,sectors;
702
703           for (pass=0; pass<2; ++pass)
704           {
705             char *s;
706
707             sectors=0;
708             for (s=argv[1]; *s; )
709             {
710               int phys;
711               char *end;
712
713               phys=strtol(s,&end,10);
714               if (pass==1) d->skewtab[sectors]=phys;
715               if (end==s)
716               {
717                 fprintf(stderr,"%s: invalid skewtab `%s' at `%s'\n",cmd,argv[1],s);
718                 exit(1);
719               }
720               s=end;
721               ++sectors;
722               if (*s==',') ++s;
723             }
724             if (pass==0) d->skewtab=malloc(sizeof(int)*sectors);
725           }
726         }
727         else if (strcmp(argv[0],"boottrk")==0) d->boottrk=strtol(argv[1],(char**)0,0);
728         else if (strcmp(argv[0],"offset")==0)  
729         {
730           off_t val;
731           unsigned int multiplier;
732           char *endptr;
733
734           errno=0;
735           multiplier=1;
736           val = strtoul(argv[1],&endptr,10);
737           if ((errno==ERANGE && val==ULONG_MAX)||(errno!=0 && val==0))
738           {
739             fprintf(stderr,"%s: invalid offset value \"%s\" - %s\n",cmd,argv[1],strerror(errno));
740             exit(1);
741           }
742           if (endptr==argv[1])
743           {
744             fprintf(stderr,"%s: offset value \"%s\" is not a number\n",cmd,argv[1]);
745             exit(1);
746           }
747           if (*endptr!='\0')
748           {
749             /* Have a unit specifier */
750             switch (toupper(*endptr))
751             {
752             case 'K':
753               multiplier=1024;
754               break;
755             case 'M':
756               multiplier=1024*1024;
757               break;
758             case 'T':
759               if (d->sectrk<0||d->tracks<0||d->secLength<0)
760               {
761                 fprintf(stderr,"%s: offset must be specified after sectrk, tracks and secLength\n",cmd);
762                 exit(1);
763               }
764               multiplier=d->sectrk*d->secLength;
765               break;
766             case 'S':
767               if (d->sectrk<0||d->tracks<0||d->secLength<0)
768               {
769                 fprintf(stderr,"%s: offset must be specified after sectrk, tracks and secLength\n",cmd);
770                 exit(1);
771               }
772               multiplier=d->secLength;
773               break;
774             default:
775               fprintf(stderr,"%s: unknown unit specifier \"%c\"\n",cmd,*endptr);
776               exit(1);
777             }
778           }
779           if (val*multiplier>INT_MAX)
780           {
781             fprintf(stderr,"%s: effective offset is out of range\n",cmd);
782             exit(1);
783           }
784           d->offset=val*multiplier;
785         }
786         else if (strcmp(argv[0],"logicalextents")==0) d->extents=strtol(argv[1],(char**)0,0);
787         else if (strcmp(argv[0],"os")==0)
788         {
789           if      (strcmp(argv[1],"2.2"  )==0) d->type|=CPMFS_DR22;
790           else if (strcmp(argv[1],"3"    )==0) d->type|=CPMFS_DR3;
791           else if (strcmp(argv[1],"isx"  )==0) d->type|=CPMFS_ISX;
792           else if (strcmp(argv[1],"p2dos")==0) d->type|=CPMFS_P2DOS;
793           else if (strcmp(argv[1],"zsys" )==0) d->type|=CPMFS_ZSYS;
794           else 
795           {
796             fprintf(stderr, "%s: invalid OS type `%s'\n", cmd, argv[1]);
797             exit(1);
798           }
799         }
800       }
801       else if (argc>0 && argv[0][0]!='#')
802       {
803         fprintf(stderr,"%s: invalid keyword `%s'\n",cmd,argv[0]);
804         exit(1);
805       }
806     }
807     else if (argc==2 && strcmp(argv[0],"diskdef")==0)
808     {
809       insideDef=1;
810       d->skew=1;
811       d->extents=0;
812       d->type|=CPMFS_DR3;
813       d->skewtab=(int*)0;
814       d->offset=0;
815       d->boottrk=d->secLength=d->sectrk=d->tracks=-1;
816       if (strcmp(argv[1],format)==0) found=1;
817     }
818   }
819   fclose(fp);
820   if (!found)
821   {
822     fprintf(stderr,"%s: unknown format %s\n",cmd,format);
823     exit(1);
824   }
825   if (d->boottrk<0)
826   {
827     fprintf(stderr, "%s: boottrk parameter invalid or missing from diskdef\n",cmd);
828     exit(1);
829   }
830   if (d->secLength<0)
831   {
832     fprintf(stderr, "%s: secLength parameter invalid or missing from diskdef\n",cmd);
833     exit(1);
834   }
835   if (d->sectrk<0)
836   {
837     fprintf(stderr, "%s: sectrk parameter invalid or missing from diskdef\n",cmd);
838     exit(1);
839   }
840   if (d->tracks<0)
841   {
842     fprintf(stderr, "%s: tracks parameter invalid or missing from diskdef\n",cmd);
843     exit(1);
844   }
845   return 0;
846 }
847 /*}}}*/
848 /* amsReadSuper       -- read super block from amstrad disk      */ /*{{{*/
849 static int amsReadSuper(struct cpmSuperBlock *d, const char *format)
850 {
851   unsigned char boot_sector[512], *boot_spec;
852   const char *err;
853
854   Device_setGeometry(&d->dev,512,9,40,0);
855   if ((err=Device_readSector(&d->dev, 0, 0, (char *)boot_sector)))
856   {
857     fprintf(stderr,"%s: Failed to read Amstrad superblock (%s)\n",cmd,err);
858     exit(1);
859   }
860   boot_spec=(boot_sector[0] == 0 || boot_sector[0] == 3)?boot_sector:(unsigned char*)0;
861   /* Check for JCE's extension to allow Amstrad and MSDOS superblocks
862    * in the same sector (for the PCW16)
863    */
864   if
865   (
866     (boot_sector[0] == 0xE9 || boot_sector[0] == 0xEB)
867     && !memcmp(boot_sector + 0x2B, "CP/M", 4)
868     && !memcmp(boot_sector + 0x33, "DSK",  3)
869     && !memcmp(boot_sector + 0x7C, "CP/M", 4)
870   ) boot_spec = boot_sector + 128;
871   if (boot_spec==(unsigned char*)0)
872   {
873     fprintf(stderr,"%s: Amstrad superblock not present\n",cmd);
874     exit(1);
875   }
876   /* boot_spec[0] = format number: 0 for SS SD, 3 for DS DD
877               [1] = single/double sided and density flags
878               [2] = cylinders per side
879               [3] = sectors per cylinder
880               [4] = Physical sector shift, 2 => 512
881               [5] = Reserved track count
882               [6] = Block shift
883               [7] = No. of directory blocks
884    */
885   d->type = 0;
886   d->type |= CPMFS_DR3; /* Amstrads are CP/M 3 systems */
887   d->secLength = 128 << boot_spec[4];
888   d->tracks    = boot_spec[2];
889   if (boot_spec[1] & 3) d->tracks *= 2;
890   d->sectrk    = boot_spec[3];
891   d->blksiz    = 128 << boot_spec[6];
892   d->maxdir    = (d->blksiz / 32) * boot_spec[7];
893   d->skew      = 1; /* Amstrads skew at the controller level */
894   d->skewtab   = (int*)0;
895   d->boottrk   = boot_spec[5];
896   d->offset    = 0;
897   d->size      = (d->secLength*d->sectrk*(d->tracks-d->boottrk))/d->blksiz;
898   d->extents   = ((d->size>=256 ? 8 : 16)*d->blksiz)/16384;
899  
900   return 0;
901 }
902 /*}}}*/
903 /* cpmCheckDs         -- read all datestamper timestamps         */ /*{{{*/
904 int cpmCheckDs(struct cpmSuperBlock *sb)
905 {
906   int dsoffset, dsblks, dsrecs, off, i;
907   unsigned char *buf;
908
909   if (!isMatching(0,"!!!TIME&","DAT",sb->dir->status,sb->dir->name,sb->dir->ext)) return -1;
910
911   /* Offset to ds file in alloc blocks */
912   dsoffset=(sb->maxdir*32+(sb->blksiz-1))/sb->blksiz;
913
914   dsrecs=(sb->maxdir+7)/8;
915   dsblks=(dsrecs*128+(sb->blksiz-1))/sb->blksiz;
916
917   /* Allocate buffer */
918   sb->ds=malloc(dsblks*sb->blksiz);
919
920   /* Read ds file in its entirety */
921   off=0;
922   for (i=dsoffset; i<dsoffset+dsblks; i++)
923   {
924     if (readBlock(sb,i,((char*)sb->ds)+off,0,-1)==-1) return -1;
925     off+=sb->blksiz;
926   }
927
928   /* Verify checksums */
929   buf = (unsigned char *)sb->ds;
930   for (i=0; i<dsrecs; i++)
931   {
932     unsigned cksum, j;
933     cksum=0;
934     for (j=0; j<127; j++) cksum += buf[j];
935     if (buf[j]!=(cksum&0xff))
936     {
937 #ifdef CPMFS_DEBUG
938       fprintf( stderr, "!!!TIME&.DAT file failed cksum at record %i\n", i );
939 #endif
940       free(sb->ds);
941       sb->ds = (struct dsDate *)0;
942       return -1;
943     }
944     buf += 128;
945   }
946   return 0;
947 }
948 /*}}}*/
949 /* cpmReadSuper       -- get DPB and init in-core data for drive */ /*{{{*/
950 int cpmReadSuper(struct cpmSuperBlock *d, struct cpmInode *root, const char *format)
951 {
952   while (s_ifdir && !S_ISDIR(s_ifdir)) s_ifdir<<=1;
953   assert(s_ifdir);
954   while (s_ifreg && !S_ISREG(s_ifreg)) s_ifreg<<=1;
955   assert(s_ifreg);
956   if (strcmp(format, "amstrad")==0) amsReadSuper(d,format);
957   else diskdefReadSuper(d,format);
958   Device_setGeometry(&d->dev,d->secLength,d->sectrk,d->tracks,d->offset);
959   if (d->skewtab==(int*)0) /* generate skew table */ /*{{{*/
960   {
961     int i,j,k;
962
963     if (( d->skewtab = malloc(d->sectrk*sizeof(int))) == (int*)0) 
964     {
965       fprintf(stderr,"%s: can not allocate memory for skew sector table\n",cmd);
966       exit(1);
967     }
968     memset(d->skewtab,0,d->sectrk*sizeof(int));
969     for (i=j=0; i<d->sectrk; ++i,j=(j+d->skew)%d->sectrk)
970     {
971       while (1)
972       {
973         for (k=0; k<i && d->skewtab[k]!=j; ++k);
974         if (k<i) j=(j+1)%d->sectrk;
975         else break;
976       }
977       d->skewtab[i]=j;
978     }
979   }
980   /*}}}*/
981   /* initialise allocation vector bitmap */ /*{{{*/
982   {
983     d->alvSize=((d->secLength*d->sectrk*(d->tracks-d->boottrk))/d->blksiz+INTBITS-1)/INTBITS;
984     if ((d->alv=malloc(d->alvSize*sizeof(int)))==(int*)0) 
985     {
986       boo="out of memory";
987       return -1;
988     }
989   }
990   /*}}}*/
991   /* allocate directory buffer */ /*{{{*/
992   if ((d->dir=malloc(d->maxdir*32))==(struct PhysDirectoryEntry*)0)
993   {
994     boo="out of memory";
995     return -1;
996   }
997   /*}}}*/
998   if (d->dev.opened==0) /* create empty directory in core */ /*{{{*/
999   {
1000     memset(d->dir,0xe5,d->maxdir*32);
1001   }
1002   /*}}}*/
1003   else /* read directory in core */ /*{{{*/
1004   {
1005     int i,blocks,entry;
1006
1007     blocks=(d->maxdir*32+d->blksiz-1)/d->blksiz;
1008     entry=0;
1009     for (i=0; i<blocks; ++i) 
1010     {
1011       if (readBlock(d,i,(char*)(d->dir+entry),0,-1)==-1) return -1;
1012       entry+=(d->blksiz/32);
1013     }
1014   }
1015   /*}}}*/
1016   alvInit(d);
1017   if (d->type&CPMFS_CPM3_OTHER) /* read additional superblock information */ /*{{{*/
1018   {
1019     int i;
1020
1021     /* passwords */ /*{{{*/
1022     {
1023       int passwords=0;
1024
1025       for (i=0; i<d->maxdir; ++i) if (d->dir[i].status>=16 && d->dir[i].status<=31) ++passwords;
1026 #ifdef CPMFS_DEBUG
1027       fprintf(stderr,"getformat: found %d passwords\n",passwords);
1028 #endif
1029       if ((d->passwdLength=passwords*PASSWD_RECLEN))
1030       {
1031         if ((d->passwd=malloc(d->passwdLength))==(char*)0)
1032         {
1033           boo="out of memory";
1034           return -1;
1035         }
1036         for (i=0,passwords=0; i<d->maxdir; ++i) if (d->dir[i].status>=16 && d->dir[i].status<=31)
1037         {
1038           int j,pb;
1039           char *p=d->passwd+(passwords++*PASSWD_RECLEN);
1040
1041           p[0]='0'+(d->dir[i].status-16)/10;
1042           p[1]='0'+(d->dir[i].status-16)%10;
1043           for (j=0; j<8; ++j) p[2+j]=d->dir[i].name[j]&0x7f;
1044           p[10]=(d->dir[i].ext[0]&0x7f)==' ' ? ' ' : '.';
1045           for (j=0; j<3; ++j) p[11+j]=d->dir[i].ext[j]&0x7f;
1046           p[14]=' ';
1047           pb=(unsigned char)d->dir[i].lrc;
1048           for (j=0; j<8; ++j) p[15+j]=((unsigned char)d->dir[i].pointers[7-j])^pb;
1049 #ifdef CPMFS_DEBUG
1050           p[23]='\0';
1051           fprintf(stderr,"getformat: %s\n",p);
1052 #endif        
1053           p[23]='\n';
1054         }
1055       }
1056     }
1057     /*}}}*/
1058     /* disc label */ /*{{{*/
1059     for (i=0; i<d->maxdir; ++i) if (d->dir[i].status==(char)0x20)
1060     {
1061       int j;
1062
1063       d->cnotatime=d->dir[i].extnol&0x10;
1064       if (d->dir[i].extnol&0x1)
1065       {
1066         d->labelLength=12;
1067         if ((d->label=malloc(d->labelLength))==(char*)0)
1068         {
1069           boo="out of memory";
1070           return -1;
1071         }
1072         for (j=0; j<8; ++j) d->label[j]=d->dir[i].name[j]&0x7f;
1073         for (j=0; j<3; ++j) d->label[8+j]=d->dir[i].ext[j]&0x7f;
1074         d->label[11]='\n';
1075       }
1076       else
1077       {
1078         d->labelLength=0;
1079       }
1080       break;
1081     }
1082     if (i==d->maxdir)
1083     {
1084       d->cnotatime=1;
1085       d->labelLength=0;
1086     }
1087     /*}}}*/
1088   }
1089   /*}}}*/
1090   else
1091   {
1092     d->passwdLength=0;
1093     d->cnotatime=1;
1094     d->labelLength=0;
1095   }
1096   d->root=root;
1097   d->dirtyDirectory = 0;
1098   root->ino=d->maxdir;
1099   root->sb=d;
1100   root->mode=(s_ifdir|0777);
1101   root->size=0;
1102   root->atime=root->mtime=root->ctime=0;
1103
1104   d->dirtyDs=0;
1105   if (cpmCheckDs(d)==0) d->type|=CPMFS_DS_DATES;
1106   else d->ds=(struct dsDate*)0;
1107
1108   return 0;
1109 }
1110 /*}}}*/
1111 /* syncDs             -- write all datestamper timestamps        */ /*{{{*/
1112 static int syncDs(const struct cpmSuperBlock *sb)
1113 {
1114   if (sb->dirtyDs)
1115   {
1116     int dsoffset, dsblks, dsrecs, off, i;
1117     unsigned char *buf;
1118     
1119     dsrecs=(sb->maxdir+7)/8;
1120
1121     /* Re-calculate checksums */
1122     buf = (unsigned char *)sb->ds;
1123     for ( i=0; i<dsrecs; i++ )
1124     {
1125       unsigned cksum, j;
1126       cksum=0;
1127       for ( j=0; j<127; j++ ) cksum += buf[j];
1128       buf[j] = cksum & 0xff;
1129       buf += 128;
1130     }
1131     dsoffset=(sb->maxdir*32+(sb->blksiz-1))/sb->blksiz;
1132     dsblks=(dsrecs*128+(sb->blksiz-1))/sb->blksiz;
1133
1134     off=0;
1135     for (i=dsoffset; i<dsoffset+dsblks; i++)
1136     {
1137       if (writeBlock(sb,i,((char*)(sb->ds))+off,0,-1)==-1) return -1;
1138       off+=sb->blksiz;
1139     }
1140   }
1141   return 0;
1142 }
1143 /*}}}*/
1144 /* cpmSync            -- write directory back                    */ /*{{{*/
1145 int cpmSync(struct cpmSuperBlock *sb)
1146 {
1147   if (sb->dirtyDirectory)
1148   {
1149     int i,blocks,entry;
1150
1151     blocks=(sb->maxdir*32+sb->blksiz-1)/sb->blksiz;
1152     entry=0;
1153     for (i=0; i<blocks; ++i) 
1154     {
1155       if (writeBlock(sb,i,(char*)(sb->dir+entry),0,-1)==-1) return -1;
1156       entry+=(sb->blksiz/32);
1157     }
1158     sb->dirtyDirectory=0;
1159   }
1160   if (sb->type&CPMFS_DS_DATES) syncDs(sb);
1161   return 0;
1162 }
1163 /*}}}*/
1164 /* cpmUmount          -- free super block                        */ /*{{{*/
1165 void cpmUmount(struct cpmSuperBlock *sb)
1166 {
1167   cpmSync(sb);
1168   if (sb->type&CPMFS_DS_DATES) free(sb->ds);
1169   free(sb->alv);
1170   free(sb->skewtab);
1171   free(sb->dir);
1172   if (sb->passwdLength) free(sb->passwd);
1173 }
1174 /*}}}*/
1175
1176 /* cpmNamei           -- map name to inode                       */ /*{{{*/
1177 int cpmNamei(const struct cpmInode *dir, const char *filename, struct cpmInode *i)
1178 {
1179   /* variables */ /*{{{*/
1180   int user;
1181   char name[8],extension[3];
1182   int highestExtno,highestExt=-1,lowestExtno,lowestExt=-1;
1183   int protectMode=0;
1184   /*}}}*/
1185
1186   if (!S_ISDIR(dir->mode))
1187   {
1188     boo="No such file";
1189     return -1;
1190   }
1191   if (strcmp(filename,".")==0 || strcmp(filename,"..")==0) /* root directory */ /*{{{*/
1192   {
1193     *i=*dir;
1194     return 0;
1195   }
1196   /*}}}*/
1197   else if (strcmp(filename,"[passwd]")==0 && dir->sb->passwdLength) /* access passwords */ /*{{{*/
1198   {
1199     i->attr=0;
1200     i->ino=dir->sb->maxdir+1;
1201     i->mode=s_ifreg|0444;
1202     i->sb=dir->sb;
1203     i->atime=i->mtime=i->ctime=0;
1204     i->size=i->sb->passwdLength;
1205     return 0;
1206   }
1207   /*}}}*/
1208   else if (strcmp(filename,"[label]")==0 && dir->sb->labelLength) /* access label */ /*{{{*/
1209   {
1210     i->attr=0;
1211     i->ino=dir->sb->maxdir+2;
1212     i->mode=s_ifreg|0444;
1213     i->sb=dir->sb;
1214     i->atime=i->mtime=i->ctime=0;
1215     i->size=i->sb->labelLength;
1216     return 0;
1217   }
1218   /*}}}*/
1219   if (splitFilename(filename,dir->sb->type,name,extension,&user)==-1) return -1;
1220   /* find highest and lowest extent */ /*{{{*/
1221   {
1222     int extent;
1223
1224     i->size=0;
1225     extent=-1;
1226     highestExtno=-1;
1227     lowestExtno=2049;
1228     while ((extent=findFileExtent(dir->sb,user,name,extension,extent+1,-1))!=-1)
1229     {
1230       int extno=EXTENT(dir->sb->dir[extent].extnol,dir->sb->dir[extent].extnoh);
1231
1232       if (extno>highestExtno)
1233       {
1234         highestExtno=extno;
1235         highestExt=extent;
1236       }
1237       if (extno<lowestExtno)
1238       {
1239         lowestExtno=extno;
1240         lowestExt=extent;
1241       }
1242     }
1243   }
1244   /*}}}*/
1245   if (highestExtno==-1) return -1;
1246   /* calculate size */ /*{{{*/
1247   {
1248     int block;
1249
1250     i->size=highestExtno*16384;
1251     if (dir->sb->size<256) for (block=15; block>=0; --block)
1252     {
1253       if (dir->sb->dir[highestExt].pointers[block]) break;
1254     }
1255     else for (block=7; block>=0; --block)
1256     {
1257       if (dir->sb->dir[highestExt].pointers[2*block] || dir->sb->dir[highestExt].pointers[2*block+1]) break;
1258     }
1259     if (dir->sb->dir[highestExt].blkcnt) i->size+=((dir->sb->dir[highestExt].blkcnt&0xff)-1)*128;
1260     if (dir->sb->type & CPMFS_ISX)
1261     {
1262       i->size += (128 - dir->sb->dir[highestExt].lrc);
1263     }
1264     else
1265     {
1266       i->size+=dir->sb->dir[highestExt].lrc ? (dir->sb->dir[highestExt].lrc&0xff) : 128;
1267     }
1268 #ifdef CPMFS_DEBUG
1269     fprintf(stderr,"cpmNamei: size=%ld\n",(long)i->size);
1270 #endif
1271   }
1272   /*}}}*/
1273   i->ino=lowestExt;
1274   i->mode=s_ifreg;
1275   i->sb=dir->sb;
1276
1277   /* read timestamps */ /*{{{*/
1278   protectMode = readTimeStamps(i,lowestExt);
1279   /*}}}*/
1280
1281   /* Determine the inode attributes */
1282   i->attr = 0;
1283   if (dir->sb->dir[lowestExt].name[0]&0x80) i->attr |= CPM_ATTR_F1;
1284   if (dir->sb->dir[lowestExt].name[1]&0x80) i->attr |= CPM_ATTR_F2;
1285   if (dir->sb->dir[lowestExt].name[2]&0x80) i->attr |= CPM_ATTR_F3;
1286   if (dir->sb->dir[lowestExt].name[3]&0x80) i->attr |= CPM_ATTR_F4;
1287   if (dir->sb->dir[lowestExt].ext [0]&0x80) i->attr |= CPM_ATTR_RO;
1288   if (dir->sb->dir[lowestExt].ext [1]&0x80) i->attr |= CPM_ATTR_SYS;
1289   if (dir->sb->dir[lowestExt].ext [2]&0x80) i->attr |= CPM_ATTR_ARCV;
1290   if (protectMode&0x20)                     i->attr |= CPM_ATTR_PWDEL;
1291   if (protectMode&0x40)                     i->attr |= CPM_ATTR_PWWRITE;
1292   if (protectMode&0x80)                     i->attr |= CPM_ATTR_PWREAD;
1293
1294   if (dir->sb->dir[lowestExt].ext[1]&0x80) i->mode|=01000;
1295   i->mode|=0444;
1296   if (!(dir->sb->dir[lowestExt].ext[0]&0x80)) i->mode|=0222;
1297   if (extension[0]=='C' && extension[1]=='O' && extension[2]=='M') i->mode|=0111;
1298
1299   readDsStamps(i,lowestExt);
1300   
1301   return 0;
1302 }
1303 /*}}}*/
1304 /* cpmStatFS          -- statfs                                  */ /*{{{*/
1305 void cpmStatFS(const struct cpmInode *ino, struct cpmStatFS *buf)
1306 {
1307   int i;
1308   struct cpmSuperBlock *d;
1309
1310   d=ino->sb;
1311   buf->f_bsize=d->blksiz;
1312   buf->f_blocks=d->size;
1313   buf->f_bfree=0;
1314   buf->f_bused=-(d->maxdir*32+d->blksiz-1)/d->blksiz;
1315   for (i=0; i<d->alvSize; ++i)
1316   {
1317     int temp,j;
1318
1319     temp = *(d->alv+i);
1320     for (j=0; j<INTBITS; ++j)
1321     {
1322       if (i*INTBITS+j < d->size)
1323       {
1324         if (1&temp)
1325         {
1326 #ifdef CPMFS_DEBUG
1327           fprintf(stderr,"cpmStatFS: block %d allocated\n",(i*INTBITS+j));
1328 #endif
1329           ++buf->f_bused;
1330         }
1331         else ++buf->f_bfree;
1332       }
1333       temp >>= 1;
1334     }
1335   }
1336   buf->f_bavail=buf->f_bfree;
1337   buf->f_files=d->maxdir;
1338   buf->f_ffree=0;
1339   for (i=0; i<d->maxdir; ++i)
1340   {
1341     if (d->dir[i].status==(char)0xe5) ++buf->f_ffree;
1342   }
1343   buf->f_namelen=11;
1344 }
1345 /*}}}*/
1346 /* cpmUnlink          -- unlink                                  */ /*{{{*/
1347 int cpmUnlink(const struct cpmInode *dir, const char *fname)
1348 {
1349   int user;
1350   char name[8],extension[3];
1351   int extent;
1352   struct cpmSuperBlock *drive;
1353
1354   if (!S_ISDIR(dir->mode))
1355   {
1356     boo="No such file";
1357     return -1;
1358   }
1359   drive=dir->sb;
1360   if (splitFilename(fname,dir->sb->type,name,extension,&user)==-1) return -1;
1361   if ((extent=findFileExtent(drive,user,name,extension,0,-1))==-1) return -1;
1362   drive->dirtyDirectory=1;
1363   drive->dir[extent].status=(char)0xe5;
1364   do
1365   {
1366     drive->dir[extent].status=(char)0xe5;
1367   } while ((extent=findFileExtent(drive,user,name,extension,extent+1,-1))>=0);
1368   alvInit(drive);
1369   return 0;
1370 }
1371 /*}}}*/
1372 /* cpmRename          -- rename                                  */ /*{{{*/
1373 int cpmRename(const struct cpmInode *dir, const char *old, const char *new)
1374 {
1375   struct cpmSuperBlock *drive;
1376   int extent;
1377   int olduser;
1378   char oldname[8], oldext[3];
1379   int newuser;
1380   char newname[8], newext[3];
1381
1382   if (!S_ISDIR(dir->mode))
1383   {
1384     boo="No such file";
1385     return -1;
1386   }
1387   drive=dir->sb;
1388   if (splitFilename(old,dir->sb->type, oldname, oldext,&olduser)==-1) return -1;
1389   if (splitFilename(new,dir->sb->type, newname, newext,&newuser)==-1) return -1;
1390   if ((extent=findFileExtent(drive,olduser,oldname,oldext,0,-1))==-1) return -1;
1391   if (findFileExtent(drive,newuser,newname, newext,0,-1)!=-1) 
1392   {
1393     boo="file already exists";
1394     return -1;
1395   }
1396   do 
1397   {
1398     drive->dirtyDirectory=1;
1399     drive->dir[extent].status=newuser;
1400     memcpy7(drive->dir[extent].name, newname, 8);
1401     memcpy7(drive->dir[extent].ext, newext, 3);
1402   } while ((extent=findFileExtent(drive,olduser,oldname,oldext,extent+1,-1))!=-1);
1403   return 0;
1404 }
1405 /*}}}*/
1406 /* cpmOpendir         -- opendir                                 */ /*{{{*/
1407 int cpmOpendir(struct cpmInode *dir, struct cpmFile *dirp)
1408 {
1409   if (!S_ISDIR(dir->mode))
1410   {
1411     boo="No such file";
1412     return -1;
1413   }
1414   dirp->ino=dir;
1415   dirp->pos=0;
1416   dirp->mode=O_RDONLY;
1417   return 0;
1418 }
1419 /*}}}*/
1420 /* cpmReaddir         -- readdir                                 */ /*{{{*/
1421 int cpmReaddir(struct cpmFile *dir, struct cpmDirent *ent)
1422 {
1423   /* variables */ /*{{{*/
1424   struct PhysDirectoryEntry *cur=(struct PhysDirectoryEntry*)0;
1425   char buf[2+8+1+3+1]; /* 00foobarxy.zzy\0 */
1426   int i;
1427   char *bufp;
1428   int hasext;
1429   /*}}}*/
1430
1431   if (!(S_ISDIR(dir->ino->mode))) /* error: not a directory */ /*{{{*/
1432   {
1433     boo="not a directory";
1434     return -1;
1435   }
1436   /*}}}*/
1437   while (1)
1438   {
1439     if (dir->pos==0) /* first entry is . */ /*{{{*/
1440     {
1441       ent->ino=dir->ino->sb->maxdir;
1442       ent->reclen=1;
1443       strcpy(ent->name,".");
1444       ent->off=dir->pos;
1445       ++dir->pos;
1446       return 1;
1447     }
1448     /*}}}*/
1449     else if (dir->pos==1) /* next entry is .. */ /*{{{*/
1450     {
1451       ent->ino=dir->ino->sb->maxdir;
1452       ent->reclen=2;
1453       strcpy(ent->name,"..");
1454       ent->off=dir->pos;
1455       ++dir->pos;
1456       return 1;
1457     }
1458     /*}}}*/
1459     else if (dir->pos==2)
1460     {
1461       if (dir->ino->sb->passwdLength) /* next entry is [passwd] */ /*{{{*/
1462       {
1463         ent->ino=dir->ino->sb->maxdir+1;
1464         ent->reclen=8;
1465         strcpy(ent->name,"[passwd]");
1466         ent->off=dir->pos;
1467         ++dir->pos;
1468         return 1;
1469       }
1470       /*}}}*/
1471     }
1472     else if (dir->pos==3)
1473     {
1474       if (dir->ino->sb->labelLength) /* next entry is [label] */ /*{{{*/
1475       {
1476         ent->ino=dir->ino->sb->maxdir+2;
1477         ent->reclen=7;
1478         strcpy(ent->name,"[label]");
1479         ent->off=dir->pos;
1480         ++dir->pos;
1481         return 1;
1482       }
1483       /*}}}*/
1484     }
1485     else if (dir->pos>=RESERVED_ENTRIES && dir->pos<dir->ino->sb->maxdir+RESERVED_ENTRIES)
1486     {
1487       int first=dir->pos-RESERVED_ENTRIES;
1488
1489       if ((cur=dir->ino->sb->dir+(dir->pos-RESERVED_ENTRIES))->status>=0 && cur->status<=(dir->ino->sb->type&CPMFS_HI_USER ? 31 : 15))
1490       {
1491         /* determine first extent for the current file */ /*{{{*/
1492         for (i=0; i<dir->ino->sb->maxdir; ++i) if (i!=(dir->pos-RESERVED_ENTRIES))
1493         {
1494           if (isMatching(cur->status,cur->name,cur->ext,dir->ino->sb->dir[i].status,dir->ino->sb->dir[i].name,dir->ino->sb->dir[i].ext) && EXTENT(cur->extnol,cur->extnoh)>EXTENT(dir->ino->sb->dir[i].extnol,dir->ino->sb->dir[i].extnoh)) first=i;
1495         }
1496         /*}}}*/
1497         if (first==(dir->pos-RESERVED_ENTRIES))
1498         {
1499           ent->ino=dir->pos-RESERVED_INODES;
1500           /* convert file name to UNIX style */ /*{{{*/
1501           buf[0]='0'+cur->status/10;
1502           buf[1]='0'+cur->status%10;
1503           for (bufp=buf+2,i=0; i<8 && (cur->name[i]&0x7f)!=' '; ++i) *bufp++=tolower(cur->name[i]&0x7f);
1504           for (hasext=0,i=0; i<3 && (cur->ext[i]&0x7f)!=' '; ++i)
1505           {
1506             if (!hasext) { *bufp++='.'; hasext=1; }
1507             *bufp++=tolower(cur->ext[i]&0x7f);
1508           }
1509           *bufp='\0';
1510           /*}}}*/
1511           assert(bufp<=buf+sizeof(buf));
1512           ent->reclen=bufp-buf;
1513           strcpy(ent->name,buf);
1514           ent->off=dir->pos;
1515           ++dir->pos;
1516           return 1;
1517         }
1518       }
1519     }
1520     else return 0;
1521     ++dir->pos;
1522   }
1523 }
1524 /*}}}*/
1525 /* cpmStat            -- stat                                    */ /*{{{*/
1526 void cpmStat(const struct cpmInode *ino, struct cpmStat *buf)
1527 {
1528   buf->ino=ino->ino;
1529   buf->mode=ino->mode;
1530   buf->size=ino->size;
1531   buf->atime=ino->atime;
1532   buf->mtime=ino->mtime;
1533   buf->ctime=ino->ctime;
1534 }
1535 /*}}}*/
1536 /* cpmOpen            -- open                                    */ /*{{{*/
1537 int cpmOpen(struct cpmInode *ino, struct cpmFile *file, mode_t mode)
1538 {
1539   if (S_ISREG(ino->mode))
1540   {
1541     if ((mode&O_WRONLY) && (ino->mode&0222)==0)
1542     {
1543       boo="permission denied";
1544       return -1;
1545     }
1546     file->pos=0;
1547     file->ino=ino;
1548     file->mode=mode;
1549     return 0;
1550   }
1551   else
1552   {
1553     boo="not a regular file";
1554     return -1;
1555   }
1556 }
1557 /*}}}*/
1558 /* cpmRead            -- read                                    */ /*{{{*/
1559 int cpmRead(struct cpmFile *file, char *buf, int count)
1560 {
1561   int findext=1,findblock=1,extent=-1,block=-1,extentno=-1,got=0,nextblockpos=-1,nextextpos=-1;
1562   int blocksize=file->ino->sb->blksiz;
1563   int extcap;
1564
1565   extcap=(file->ino->sb->size<256 ? 16 : 8)*blocksize;
1566   if (extcap>16384) extcap=16384*file->ino->sb->extents;
1567   if (file->ino->ino==file->ino->sb->maxdir+1) /* [passwd] */ /*{{{*/
1568   {
1569     if ((file->pos+count)>file->ino->size) count=file->ino->size-file->pos;
1570     if (count) memcpy(buf,file->ino->sb->passwd+file->pos,count);
1571     file->pos+=count;
1572 #ifdef CPMFS_DEBUG
1573     fprintf(stderr,"cpmRead passwd: read %d bytes, now at position %ld\n",count,(long)file->pos);
1574 #endif
1575     return count;
1576   }
1577   /*}}}*/
1578   else if (file->ino->ino==file->ino->sb->maxdir+2) /* [label] */ /*{{{*/
1579   {
1580     if ((file->pos+count)>file->ino->size) count=file->ino->size-file->pos;
1581     if (count) memcpy(buf,file->ino->sb->label+file->pos,count);
1582     file->pos+=count;
1583 #ifdef CPMFS_DEBUG
1584     fprintf(stderr,"cpmRead label: read %d bytes, now at position %ld\n",count,(long)file->pos);
1585 #endif
1586     return count;
1587   }
1588   /*}}}*/
1589   else while (count>0 && file->pos<file->ino->size)
1590   {
1591     char buffer[16384];
1592
1593     if (findext)
1594     {
1595       extentno=file->pos/16384;
1596       extent=findFileExtent(file->ino->sb,file->ino->sb->dir[file->ino->ino].status,file->ino->sb->dir[file->ino->ino].name,file->ino->sb->dir[file->ino->ino].ext,0,extentno);
1597       nextextpos=(file->pos/extcap)*extcap+extcap;
1598       findext=0;
1599       findblock=1;
1600     }
1601     if (findblock)
1602     {
1603       if (extent!=-1)
1604       {
1605         int start,end,ptr;
1606
1607         ptr=(file->pos%extcap)/blocksize;
1608         if (file->ino->sb->size>=256) ptr*=2;
1609         block=(unsigned char)file->ino->sb->dir[extent].pointers[ptr];
1610         if (file->ino->sb->size>=256) block+=((unsigned char)file->ino->sb->dir[extent].pointers[ptr+1])<<8;
1611         if (block==0)
1612         {
1613           memset(buffer,0,blocksize);
1614         }
1615         else
1616         {
1617           start=(file->pos%blocksize)/file->ino->sb->secLength;
1618           end=((file->pos%blocksize+count)>blocksize ? blocksize-1 : (file->pos%blocksize+count-1))/file->ino->sb->secLength;
1619           readBlock(file->ino->sb,block,buffer,start,end);
1620         }
1621       }
1622       nextblockpos=(file->pos/blocksize)*blocksize+blocksize;
1623       findblock=0;
1624     }
1625     if (file->pos<nextblockpos)
1626     {
1627       if (extent==-1) *buf++='\0'; else *buf++=buffer[file->pos%blocksize];
1628       ++file->pos;
1629       ++got;
1630       --count;
1631     }
1632     else if (file->pos==nextextpos) findext=1; else findblock=1;
1633   }
1634 #ifdef CPMFS_DEBUG
1635   fprintf(stderr,"cpmRead: read %d bytes, now at position %ld\n",got,(long)file->pos);
1636 #endif
1637   return got;
1638 }
1639 /*}}}*/
1640 /* cpmWrite           -- write                                   */ /*{{{*/
1641 int cpmWrite(struct cpmFile *file, const char *buf, int count)
1642 {
1643   int findext=1,findblock=-1,extent=-1,extentno=-1,got=0,nextblockpos=-1,nextextpos=-1;
1644   int blocksize=file->ino->sb->blksiz;
1645   int extcap=(file->ino->sb->size<256 ? 16 : 8)*blocksize;
1646   int block=-1,start=-1,end=-1,ptr=-1,last=-1;
1647   char buffer[16384];
1648
1649   while (count>0)
1650   {
1651     if (findext) /*{{{*/
1652     {
1653       extentno=file->pos/16384;
1654       extent=findFileExtent(file->ino->sb,file->ino->sb->dir[file->ino->ino].status,file->ino->sb->dir[file->ino->ino].name,file->ino->sb->dir[file->ino->ino].ext,0,extentno);
1655       nextextpos=(file->pos/extcap)*extcap+extcap;
1656       if (extent==-1)
1657       {
1658         if ((extent=findFreeExtent(file->ino->sb))==-1) return (got==0 ? -1 : got);
1659         file->ino->sb->dir[extent]=file->ino->sb->dir[file->ino->ino];
1660         memset(file->ino->sb->dir[extent].pointers,0,16);
1661         file->ino->sb->dir[extent].extnol=EXTENTL(extentno);
1662         file->ino->sb->dir[extent].extnoh=EXTENTH(extentno);
1663         file->ino->sb->dir[extent].blkcnt=0;
1664         file->ino->sb->dir[extent].lrc=0;
1665         time(&file->ino->ctime);
1666         updateTimeStamps(file->ino,extent);
1667         updateDsStamps(file->ino,extent);
1668       }
1669       findext=0;
1670       findblock=1;
1671     }
1672     /*}}}*/
1673     if (findblock) /*{{{*/
1674     {
1675       ptr=(file->pos%extcap)/blocksize;
1676       if (file->ino->sb->size>=256) ptr*=2;
1677       block=(unsigned char)file->ino->sb->dir[extent].pointers[ptr];
1678       if (file->ino->sb->size>=256) block+=((unsigned char)file->ino->sb->dir[extent].pointers[ptr+1])<<8;
1679       if (block==0) /* allocate new block, set start/end to cover it */ /*{{{*/
1680       {
1681         if ((block=allocBlock(file->ino->sb))==-1) return (got==0 ? -1 : got);
1682         file->ino->sb->dir[extent].pointers[ptr]=block&0xff;
1683         if (file->ino->sb->size>=256) file->ino->sb->dir[extent].pointers[ptr+1]=(block>>8)&0xff;
1684         start=0;
1685         end=(blocksize-1)/file->ino->sb->secLength;
1686         memset(buffer,0,blocksize);
1687         time(&file->ino->ctime);
1688         updateTimeStamps(file->ino,extent);
1689         updateDsStamps(file->ino,extent);
1690       }
1691       /*}}}*/
1692       else /* read existing block and set start/end to cover modified parts */ /*{{{*/
1693       {
1694         start=(file->pos%blocksize)/file->ino->sb->secLength;
1695         end=((file->pos%blocksize+count)>blocksize ? blocksize-1 : (file->pos%blocksize+count-1))/file->ino->sb->secLength;
1696         if (file->pos%file->ino->sb->secLength) readBlock(file->ino->sb,block,buffer,start,start);
1697         if (end!=start && (file->pos+count-1)<blocksize) readBlock(file->ino->sb,block,buffer+end*file->ino->sb->secLength,end,end);
1698       }
1699       /*}}}*/
1700       nextblockpos=(file->pos/blocksize)*blocksize+blocksize;
1701       findblock=0;
1702     }
1703     /*}}}*/
1704     /* fill block and write it */ /*{{{*/
1705     file->ino->sb->dirtyDirectory=1;
1706     while (file->pos!=nextblockpos && count)
1707     {
1708       buffer[file->pos%blocksize]=*buf++;
1709       ++file->pos;
1710       if (file->ino->size<file->pos) file->ino->size=file->pos;
1711       ++got;
1712       --count;
1713     }
1714     (void)writeBlock(file->ino->sb,block,buffer,start,end);
1715     time(&file->ino->mtime);
1716     if (file->ino->sb->size<256) for (last=15; last>=0; --last)
1717     {
1718       if (file->ino->sb->dir[extent].pointers[last])
1719       {
1720         break;
1721       }
1722     }
1723     else for (last=14; last>0; last-=2)
1724     {
1725       if (file->ino->sb->dir[extent].pointers[last] || file->ino->sb->dir[extent].pointers[last+1])
1726       {
1727         last/=2;
1728         break;
1729       }
1730     }
1731     if (last>0) extentno+=(last*blocksize)/extcap;
1732     file->ino->sb->dir[extent].extnol=EXTENTL(extentno);
1733     file->ino->sb->dir[extent].extnoh=EXTENTH(extentno);
1734     file->ino->sb->dir[extent].blkcnt=((file->pos-1)%16384)/128+1;
1735     if (file->ino->sb->type & CPMFS_EXACT_SIZE)
1736     {
1737       file->ino->sb->dir[extent].lrc = (128 - (file->pos%128)) & 0x7F;
1738     }
1739     else
1740     {
1741       file->ino->sb->dir[extent].lrc=file->pos%128;
1742     }
1743     updateTimeStamps(file->ino,extent);
1744     updateDsStamps(file->ino,extent);
1745     /*}}}*/
1746     if (file->pos==nextextpos) findext=1;
1747     else if (file->pos==nextblockpos) findblock=1;
1748   }
1749   return got;
1750 }
1751 /*}}}*/
1752 /* cpmClose           -- close                                   */ /*{{{*/
1753 int cpmClose(struct cpmFile *file)
1754 {
1755   return 0;
1756 }
1757 /*}}}*/
1758 /* cpmCreat           -- creat                                   */ /*{{{*/
1759 int cpmCreat(struct cpmInode *dir, const char *fname, struct cpmInode *ino, mode_t mode)
1760 {
1761   int user;
1762   char name[8],extension[3];
1763   int extent;
1764   struct cpmSuperBlock *drive;
1765   struct PhysDirectoryEntry *ent;
1766
1767   if (!S_ISDIR(dir->mode))
1768   {
1769     boo="No such file or directory";
1770     return -1;
1771   }
1772   if (splitFilename(fname,dir->sb->type,name,extension,&user)==-1) return -1;
1773 #ifdef CPMFS_DEBUG
1774   fprintf(stderr,"cpmCreat: %s -> %d:%-.8s.%-.3s\n",fname,user,name,extension);
1775 #endif
1776   if (findFileExtent(dir->sb,user,name,extension,0,-1)!=-1) return -1;
1777   drive=dir->sb;
1778   if ((extent=findFreeExtent(dir->sb))==-1) return -1;
1779   ent=dir->sb->dir+extent;
1780   drive->dirtyDirectory=1;
1781   memset(ent,0,32);
1782   ent->status=user;
1783   memcpy(ent->name,name,8);
1784   memcpy(ent->ext,extension,3);
1785   ino->ino=extent;
1786   ino->mode=s_ifreg|mode;
1787   ino->size=0;
1788
1789   time(&ino->atime);
1790   time(&ino->mtime);
1791   time(&ino->ctime);
1792   ino->sb=dir->sb;
1793   updateTimeStamps(ino,extent);
1794   updateDsStamps(ino,extent);
1795   return 0;
1796 }
1797 /*}}}*/
1798
1799 /* cpmAttrGet         -- get CP/M attributes                     */ /*{{{*/
1800 int cpmAttrGet(struct cpmInode *ino, cpm_attr_t *attrib)
1801 {
1802   *attrib = ino->attr;
1803   return 0;
1804 }
1805 /*}}}*/
1806 /* cpmAttrSet         -- set CP/M attributes                     */ /*{{{*/
1807 int cpmAttrSet(struct cpmInode *ino, cpm_attr_t attrib)
1808 {
1809   struct cpmSuperBlock *drive;
1810   int extent;
1811   int user;
1812   char name[8], extension[3];
1813   
1814   memset(name,      0, sizeof(name));
1815   memset(extension, 0, sizeof(extension));
1816   drive  = ino->sb;
1817   extent = ino->ino;
1818   
1819   drive->dirtyDirectory=1;
1820   /* Strip off existing attribute bits */
1821   memcpy7(name,      drive->dir[extent].name, 8);
1822   memcpy7(extension, drive->dir[extent].ext,  3);
1823   user = drive->dir[extent].status;
1824   
1825   /* And set new ones */
1826   if (attrib & CPM_ATTR_F1)   name[0]      |= 0x80;
1827   if (attrib & CPM_ATTR_F2)   name[1]      |= 0x80;
1828   if (attrib & CPM_ATTR_F3)   name[2]      |= 0x80;
1829   if (attrib & CPM_ATTR_F4)   name[3]      |= 0x80;
1830   if (attrib & CPM_ATTR_RO)   extension[0] |= 0x80;
1831   if (attrib & CPM_ATTR_SYS)  extension[1] |= 0x80;
1832   if (attrib & CPM_ATTR_ARCV) extension[2] |= 0x80;
1833   
1834   do 
1835   {
1836     memcpy(drive->dir[extent].name, name, 8);
1837     memcpy(drive->dir[extent].ext, extension, 3);
1838   } while ((extent=findFileExtent(drive, user,name,extension,extent+1,-1))!=-1);
1839
1840   /* Update the stored (inode) copies of the file attributes and mode */
1841   ino->attr=attrib;
1842   if (attrib&CPM_ATTR_RO) ino->mode&=~(S_IWUSR|S_IWGRP|S_IWOTH);
1843   else ino->mode|=(S_IWUSR|S_IWGRP|S_IWOTH);
1844   
1845   return 0;
1846 }
1847 /*}}}*/
1848 /* cpmChmod           -- set CP/M r/o & sys                      */ /*{{{*/
1849 int cpmChmod(struct cpmInode *ino, mode_t mode)
1850 {
1851   /* Convert the chmod() into a chattr() call that affects RO */
1852   int newatt = ino->attr & ~CPM_ATTR_RO;
1853
1854   if (!(mode & (S_IWUSR|S_IWGRP|S_IWOTH))) newatt |= CPM_ATTR_RO;
1855   return cpmAttrSet(ino, newatt);
1856 }
1857 /*}}}*/
1858 /* cpmUtime           -- set timestamps                          */ /*{{{*/
1859 void cpmUtime(struct cpmInode *ino, struct utimbuf *times)
1860 {
1861   ino->atime = times->actime;
1862   ino->mtime = times->modtime;
1863   time(&ino->ctime);
1864   updateTimeStamps(ino,ino->ino);
1865   updateDsStamps(ino,ino->ino);
1866 }
1867 /*}}}*/