|
|
1.1 root 1: /*
2: dblspace_dec.c
3:
4: DMSDOS CVF-FAT module: [dbl|drv]space cluster read and decompression routines.
5:
6: ******************************************************************************
7: DMSDOS (compressed MSDOS filesystem support) for Linux
8: written 1995-1998 by Frank Gockel and Pavel Pisa
9:
10: (C) Copyright 1995-1998 by Frank Gockel
11: (C) Copyright 1996-1998 by Pavel Pisa
12:
13: Some code of dmsdos has been copied from the msdos filesystem
14: so there are the following additional copyrights:
15:
16: (C) Copyright 1992,1993 by Werner Almesberger (msdos filesystem)
17: (C) Copyright 1994,1995 by Jacques Gelinas (mmap code)
18: (C) Copyright 1992-1995 by Linus Torvalds
19:
20: DMSDOS was inspired by the THS filesystem (a simple doublespace
21: DS-0-2 compressed read-only filesystem) written 1994 by Thomas Scheuermann.
22:
23: The DMSDOS code is distributed under the Gnu General Public Licence.
24: See file COPYING for details.
25: ******************************************************************************
26:
27: */
28:
29: #ifdef __KERNEL__
30: #include <linux/kernel.h>
31: #include <linux/sched.h>
32: #include <linux/errno.h>
33: #include <linux/string.h>
34: #include <linux/stat.h>
35: #include <linux/mm.h>
36: #include <linux/locks.h>
37: #include <linux/fs.h>
38: #include <linux/malloc.h>
39: #include <linux/msdos_fs.h>
40: #include <asm/system.h>
41: #include <asm/segment.h>
42: #include <asm/bitops.h>
43: #include <asm/byteorder.h>
44: #endif
45:
46: #include "dmsdos.h"
47:
48: #ifdef __DMSDOS_LIB__
49: /* some interface hacks */
50: #include"lib_interface.h"
51: #include<malloc.h>
52: #include<string.h>
53: #include<errno.h>
54: #endif
55:
1.1.1.2 ! root 56: #ifdef __GNUC__
1.1 root 57: #define INLINE static inline
1.1.1.2 ! root 58: #else
! 59: /* non-gnu compilers may not like inline */
! 60: #define INLINE static
! 61: #endif
1.1 root 62:
63: /* we always need DS decompression */
64:
65: #if defined(__GNUC__) && defined(__i386__) && defined(USE_ASM)
66: #define USE_GNU_ASM_i386
67:
68: /* copy block, overlaping part is replaced by repeat of previous part */
69: /* pointers and counter are modified to point after block */
70: #define M_MOVSB(D,S,C) \
71: __asm__ /*__volatile__*/(\
72: "cld\n\t" \
73: "rep\n\t" \
74: "movsb\n" \
75: :"=D" (D),"=S" (S),"=c" (C) \
76: :"0" (D),"1" (S),"2" (C) \
77: :"memory")
78:
79:
80: #else
81:
1.1.1.2 ! root 82: #ifdef __GNUC__
! 83: /* non-gnu compilers may not like warning directive */
1.1 root 84: #warning USE_GNU_ASM_I386 not defined, using "C" equivalent
1.1.1.2 ! root 85: #endif
1.1 root 86:
87: #define M_MOVSB(D,S,C) for(;(C);(C)--) *((__u8*)(D)++)=*((__u8*)(S)++)
88:
89: #endif
90:
91: #if !defined(le16_to_cpu)
92: /* for old kernel versions - works only on i386 */
93: #define le16_to_cpu(v) (v)
94: #endif
95:
96: /* for reading and writting from/to bitstream */
97: typedef
98: struct {
99: __u32 buf; /* bit buffer */
100: int pb; /* already readed bits from buf */
101: __u16 *pd; /* first not readed input data */
102: __u16 *pe; /* after end of data */
103: } bits_t;
104:
105: const unsigned dblb_bmsk[]=
106: {0x0,0x1,0x3,0x7,0xF,0x1F,0x3F,0x7F,0xFF,
107: 0x1FF,0x3FF,0x7FF,0xFFF,0x1FFF,0x3FFF,0x7FFF,0xFFFF};
108:
109: /* read next 16 bits from input */
110: #define RDN_G16(bits) \
111: { \
112: (bits).buf>>=16; \
113: (bits).pb-=16; \
114: if((bits).pd<(bits).pe) \
115: { \
116: (bits).buf|=((__u32)(le16_to_cpu(*((bits).pd++))))<<16; \
117: }; \
118: }
119:
120: /* prepares at least 16 bits for reading */
121: #define RDN_PR(bits,u) \
122: { \
123: if((bits).pb>=16) RDN_G16(bits); \
124: u=(bits).buf>>(bits).pb; \
125: }
126:
127: /* initializes reading from bitstream */
128: INLINE void dblb_rdi(bits_t *pbits,void *pin,unsigned lin)
129: {
130: pbits->pb=32;
131: pbits->pd=(__u16*)pin;
132: pbits->pe=pbits->pd+((lin+1)>>1);
133: }
134:
135: /* reads n<=16 bits from bitstream *pbits */
136: INLINE unsigned dblb_rdn(bits_t *pbits,int n)
137: {
138: unsigned u;
139: RDN_PR(*pbits,u);
140: pbits->pb+=n;
141: u&=dblb_bmsk[n];
142: return u;
143: }
144:
145: INLINE int dblb_rdoffs(bits_t *pbits)
146: { unsigned u;
147: RDN_PR(*pbits,u);
148: switch (u&3)
149: {
150: case 0: case 2:
151: pbits->pb+=1+6; return 63&(u>>1);
152: case 1:
153: pbits->pb+=2+8; return (255&(u>>2))+64;
154: }
155: pbits->pb+=2+12; return (4095&(u>>2))+320;
156: }
157:
158: INLINE int dblb_rdlen(bits_t *pbits)
159: { unsigned u;
160: RDN_PR(*pbits,u);
161: switch (u&15)
162: { case 1: case 3: case 5: case 7:
163: case 9: case 11: case 13: case 15:
164: pbits->pb++; return 3;
165: case 2: case 6:
166: case 10: case 14:
167: pbits->pb+=2+1; return (1&(u>>2))+4;
168: case 4: case 12:
169: pbits->pb+=3+2; return (3&(u>>3))+6;
170: case 8:
171: pbits->pb+=4+3; return (7&(u>>4))+10;
172: case 0: ;
173: }
174: switch ((u>>4)&15)
175: { case 1: case 3: case 5: case 7:
176: case 9: case 11: case 13: case 15:
177: pbits->pb+=5+4; return (15&(u>>5))+18;
178: case 2: case 6:
179: case 10: case 14:
180: pbits->pb+=6+5; return (31&(u>>6))+34;
181: case 4: case 12:
182: pbits->pb+=7+6; return (63&(u>>7))+66;
183: case 8:
184: pbits->pb+=8+7; return (127&(u>>8))+130;
185: case 0: ;
186: }
187: pbits->pb+=9;
188: if(u&256) return dblb_rdn(pbits,8)+258;
189: return -1;
190: }
191:
192: INLINE int dblb_decrep(bits_t *pbits, __u8 **p, void *pout, __u8 *pend,
193: int repoffs, int k, int flg)
194: { int replen;
195: __u8 *r;
196:
197: if(repoffs==0){LOG_DECOMP("DMSDOS: decrb: zero offset ?\n");return -2;}
198: if(repoffs==0x113f)
199: {
200: int pos=*p-(__u8*)pout;
201: LOG_DECOMP("DMSDOS: decrb: 0x113f sync found.\n");
202: if((pos%512) && !(flg&0x4000))
203: { LOG_DECOMP("DMSDOS: decrb: sync at decompressed pos %d ?\n",pos);
204: return -2;
205: }
206: return 0;
207: }
208: replen=dblb_rdlen(pbits)+k;
209:
210: if(replen<=0)
211: {LOG_DECOMP("DMSDOS: decrb: illegal count ?\n");return -2;}
212: if((__u8*)pout+repoffs>*p)
213: {LOG_DECOMP("DMSDOS: decrb: of>pos ?\n");return -2;}
214: if(*p+replen>pend)
215: {LOG_DECOMP("DMSDOS: decrb: output overfill ?\n");return -2;}
216: r=*p-repoffs;
217: M_MOVSB(*p,r,replen);
218: return 0;
219: }
220:
221: /* DS decompression */
222: /* flg=0x4000 is used, when called from stacker_dec.c, because of
223: stacker does not store original cluster size and it can mean,
224: that last cluster in file can be ended by garbage */
225: int ds_dec(void* pin,int lin, void* pout, int lout, int flg)
226: {
227: __u8 *p, *pend;
228: unsigned u, repoffs;
229: int r;
230: bits_t bits;
231:
232: dblb_rdi(&bits,pin,lin);
233: p=(__u8*)pout;pend=p+lout;
234: if((dblb_rdn(&bits,16))!=0x5344) return -1;
235:
236: u=dblb_rdn(&bits,16);
237: LOG_DECOMP("DMSDOS: DS decompression version %d\n",u);
238:
239: do
240: { r=0;
241: RDN_PR(bits,u);
242: switch(u&3)
243: {
244: case 0:
245: bits.pb+=2+6;
246: repoffs=(u>>2)&63;
247: r=dblb_decrep(&bits,&p,pout,pend,repoffs,-1,flg);
248: break;
249: case 1:
250: bits.pb+=2+7;
251: *(p++)=(u>>2)|128;
252: break;
253: case 2:
254: bits.pb+=2+7;
255: *(p++)=(u>>2)&127;
256: break;
257: case 3:
258: if(u&4) { bits.pb+=3+12; repoffs=((u>>3)&4095)+320; }
259: else { bits.pb+=3+8; repoffs=((u>>3)&255)+64; };
260: r=dblb_decrep(&bits,&p,pout,pend,repoffs,-1,flg);
261: break;
262: }
263: }while((r==0)&&(p<pend));
264:
265: if(r<0) return r;
266:
267: if(!(flg&0x4000))
268: {
269: u=dblb_rdn(&bits,3);if(u==7) u=dblb_rdn(&bits,12)+320;
270: if(u!=0x113f)
271: { LOG_DECOMP("DMSDOS: decrb: final sync not found?\n");
272: return -2;
273: }
274: }
275:
276: return p-(__u8*)pout;
277: }
278:
279: /* JM decompression */
280: int jm_dec(void* pin,int lin, void* pout, int lout, int flg)
281: {
282: __u8 *p, *pend;
283: unsigned u, repoffs;
284: int r;
285: bits_t bits;
286:
287: dblb_rdi(&bits,pin,lin);
288: p=(__u8*)pout;pend=p+lout;
289: if((dblb_rdn(&bits,16))!=0x4D4A) return -1;
290:
291: u=dblb_rdn(&bits,16);
292: LOG_DECOMP("DMSDOS: JM decompression version %d\n",u);
293:
294: do
295: { r=0;
296: RDN_PR(bits,u);
297: switch(u&3)
298: {
299: case 0:
300: case 2:
301: bits.pb+=8;
302: *(p++)=(u>>1)&127;
303: break;
304: case 1:
305: bits.pb+=2;
306: repoffs=dblb_rdoffs(&bits);
307: r=dblb_decrep(&bits,&p,pout,pend,repoffs,0,flg);
308: break;
309: case 3:
310: bits.pb+=9;
311: *(p++)=((u>>2)&127)|128;
312: break;
313: }
314: }
315: while((r==0)&&(p<pend));
316:
317: if(r<0) return r;
318:
319: if(!(flg&0x4000))
320: {
321: u=dblb_rdn(&bits,2);if(u==1) u=dblb_rdoffs(&bits);
322: if(u!=0x113f)
323: { LOG_DECOMP("DMSDOS: decrb: final sync not found?\n");
324: return -2;
325: }
326: }
327:
328: return p-(__u8*)pout;
329: }
330:
331:
332: /* decompress a compressed doublespace/drivespace cluster clusterk to clusterd
333: */
334: int dbl_decompress(unsigned char*clusterd, unsigned char*clusterk,
335: Mdfat_entry*mde)
336: {
337: int sekcount;
338: int r, lin, lout;
339:
340: sekcount=mde->size_hi_minus_1+1;
341: lin=(mde->size_lo_minus_1+1)*SECTOR_SIZE;
342: lout=(mde->size_hi_minus_1+1)*SECTOR_SIZE;
343:
344: switch(clusterk[0]+((int)clusterk[1]<<8)+
345: ((int)clusterk[2]<<16)+((int)clusterk[3]<<24))
346: {
347: case DS_0_0:
348: case DS_0_1:
349: case DS_0_2:
350: LOG_DECOMP("DMSDOS: decompressing DS-0-x\n");
351: r=ds_dec(clusterk,lin,clusterd,lout,0);
352: if(r<=0)
353: { printk(KERN_ERR "DMSDOS: error in DS-0-x compressed data.\n");
354: return -2;
355: }
356: LOG_DECOMP("DMSDOS: decompress finished.\n");
357: return 0;
358:
359: case JM_0_0:
360: case JM_0_1:
361: LOG_DECOMP("DMSDOS: decompressing JM-0-x\n");
362: r=jm_dec(clusterk,lin,clusterd,lout,0);
363: if(r<=0)
364: { printk(KERN_ERR "DMSDOS: error in JM-0-x compressed data.\n");
365: return -2;
366: }
367: LOG_DECOMP("DMSDOS: decompress finished.\n");
368: return 0;
369:
370: #ifdef DMSDOS_CONFIG_DRVSP3
371: case SQ_0_0:
372: LOG_DECOMP("DMSDOS: decompressing SQ-0-0\n");
373: r=sq_dec(clusterk,lin,clusterd,lout,0);
374: if(r<=0)
375: { printk(KERN_ERR "DMSDOS: SQ-0-0 decompression failed.\n");
376: return -1;
377: }
378: LOG_DECOMP("DMSDOS: decompress finished.\n");
379: return 0;
380: #endif
381:
382: default:
383: printk(KERN_ERR "DMSDOS: compression method not recognized.\n");
384: return -1;
385:
386: } /* end switch */
387:
388: return 0;
389: }
390:
391: #ifdef DMSDOS_CONFIG_DRVSP3
392: /* read the fragments of a fragmented cluster and assemble them */
393: /* warning: this is guessed from low level viewing drivespace 3 disks
394: and may be awfully wrong... we'll see... */
395: int read_fragments(struct super_block*sb,Mdfat_entry*mde, unsigned char*data)
396: { struct buffer_head*bh;
397: struct buffer_head*bh2;
398: int fragcount;
399: int fragpnt;
400: int offset;
401: int sector;
402: int seccount;
403: int membytes;
404: int safety_counter;
405: Dblsb*dblsb=MSDOS_SB(sb)->private_data;
406:
407: /* read first sector */
408: sector=mde->sector_minus_1+1;
409: bh=raw_bread(sb,sector);
410: if(bh==NULL)return -EIO;
411: fragcount=bh->b_data[0];
412: if(bh->b_data[1]!=0||bh->b_data[2]!=0||bh->b_data[3]!=0||fragcount<=0||
413: fragcount>dblsb->s_sectperclust)
414: { printk(KERN_ERR "DMSDOS: read_fragments: cluster does not look fragmented!\n");
415: raw_brelse(sb,bh);
416: return -EIO;
417: }
418: membytes=dblsb->s_sectperclust*SECTOR_SIZE;
419: if(mde->flags&1)
420: { offset=0;
421: safety_counter=0;
422: }
423: else
424: { offset=(fragcount+1)*4;
425: /* copy the rest of the sector */
426: memcpy(data,&(bh->b_data[offset]),SECTOR_SIZE-offset);
427: data+=(SECTOR_SIZE-offset);
428: safety_counter=SECTOR_SIZE-offset;
429: }
430: ++sector;
431: seccount=mde->size_lo_minus_1;
432: fragpnt=1;
433: while(fragpnt<=fragcount)
434: { if(fragpnt>1)
435: { /* read next fragment pointers */
436: seccount=bh->b_data[fragpnt*4+3];
437: seccount&=0xff;
438: seccount/=4;
439: seccount+=1;
440: sector=bh->b_data[fragpnt*4];
441: sector&=0xff;
442: sector+=bh->b_data[fragpnt*4+1]<<8;
443: sector&=0xffff;
444: sector+=bh->b_data[fragpnt*4+2]<<16;
445: sector&=0xffffff;
446: sector+=1;
447: }
448: while(seccount)
449: { bh2=raw_bread(sb,sector);
450: if(bh2==NULL){raw_brelse(sb,bh);return -EIO;}
451: /*printk(KERN_DEBUG "DMSDOS: read_fragments: data=0x%p safety_counter=0x%x sector=%d\n",
452: data,safety_counter,sector);*/
453: if(safety_counter+SECTOR_SIZE>membytes)
454: { int maxbytes=membytes-safety_counter;
455: if(maxbytes<=0)
456: { printk(KERN_WARNING "DMSDOS: read_fragments: safety_counter exceeds membytes!\n");
457: raw_brelse(sb,bh2);
458: raw_brelse(sb,bh);
459: return -EIO;
460: }
461: printk(KERN_DEBUG "DMSDOS: read_fragments: size limit reached.\n");
462: memcpy(data,bh2->b_data,maxbytes);
463: raw_brelse(sb,bh2);
464: raw_brelse(sb,bh);
465: return membytes;
466: }
467: else memcpy(data,bh2->b_data,SECTOR_SIZE);
468: raw_brelse(sb,bh2);
469: data+=SECTOR_SIZE;
470: safety_counter+=SECTOR_SIZE;
471: ++sector;
472: --seccount;
473: }
474: ++fragpnt;
475: }
476: raw_brelse(sb,bh);
477:
478: return safety_counter;
479: }
480: #endif
481:
482: #ifdef DMSDOS_CONFIG_DBL
483: /* read a complete file cluster and decompress it if necessary;
484: this function is unable to read cluster 0 (CVF root directory) */
485: /* returns cluster length in bytes or error (<0) */
486: /* this function is specific to doublespace/drivespace */
487: int dbl_read_cluster(struct super_block*sb,
488: unsigned char*clusterd, int clusternr)
489: { Mdfat_entry mde;
490: unsigned char*clusterk;
491: int nr_of_sectors;
492: int i;
493: struct buffer_head*bh;
494: int membytes;
495: int sector;
496: Dblsb*dblsb=MSDOS_SB(sb)->private_data;
497:
498: LOG_CLUST("DMSDOS: dbl_read_cluster %d\n",clusternr);
499:
500: dbl_mdfat_value(sb,clusternr,NULL,&mde);
501:
502: if((mde.flags&2)==0)
503: { /* hmm, cluster is unused (it's a lost or ghost cluster)
504: and contains undefined data, but it *is* readable */
505: /* oh no, it contains ZEROD data per definition...
506: this is really important */
507: if(clusterd) /*clusterd==NULL means read_ahead - don't do anything*/
508: memset(clusterd,0,dblsb->s_sectperclust*SECTOR_SIZE);
509: LOG_CLUST("DMSDOS: lost cluster %d detected\n",clusternr);
510: return 0; /* yes, has length zero */
511: }
512:
513: sector=mde.sector_minus_1+1;
514: nr_of_sectors=mde.size_lo_minus_1+1;/* real sectors on disk */
515: if(nr_of_sectors>dblsb->s_sectperclust)
516: { printk(KERN_WARNING "DMSDOS: read_cluster: mdfat sectors > sectperclust, cutting\n");
517: nr_of_sectors=dblsb->s_sectperclust;
518: }
519:
520: if(clusterd==NULL)
521: { /* read-ahead */
522: dblspace_reada(sb,sector,nr_of_sectors);
523: return 0;
524: }
525:
526: #ifdef DMSDOS_CONFIG_DRVSP3
527: if(mde.unknown&2)
528: { /* we suppose this bit indicates a fragmented cluster */
529: /* this is *not sure* and may be awfully wrong - reports
530: whether success or not are welcome
531: */
532:
533: LOG_CLUST("DMSDOS: cluster %d has unknown bit #1 set. Assuming fragmented cluster.\n",
534: clusternr);
535:
536: if(mde.flags&1) /* not compressed */
537: { LOG_CLUST("DMSDOS: uncompressed fragmented cluster\n");
538: i=read_fragments(sb,&mde,clusterd);
539: if(i<0)
540: { printk(KERN_ERR "DMSDOS: read_fragments failed!\n");
541: return i;
542: }
543: }
544: else
545: { LOG_CLUST("DMSDOS: compressed fragmented cluster\n");
546: membytes=SECTOR_SIZE*dblsb->s_sectperclust;
547:
548: clusterk=(unsigned char*)MALLOC(membytes);
549: if(clusterk==NULL)
550: { printk(KERN_ERR "DMSDOS: no memory for decompression!\n");
551: return -2;
552: }
553: /* returns length in bytes */
554: i=read_fragments(sb,&mde,clusterk);
555: if(i<0)
556: { printk(KERN_ERR "DMSDOS: read_fragments failed!\n");
557: return i;
558: }
559: /* correct wrong size_lo information (sq_dec needs it) */
560: if(i>0)mde.size_lo_minus_1=(i-1)/SECTOR_SIZE;
561: i=dbl_decompress(clusterd,clusterk,&mde);
562:
563: FREE(clusterk);
564:
565: if(i)
566: { printk(KERN_ERR "DMSDOS: decompression of cluster %d in CVF failed.\n",
567: clusternr);
568: return i;
569: }
570:
571: }
572:
573: /* the slack must be zerod out */
574: if(mde.size_hi_minus_1+1<dblsb->s_sectperclust)
575: { memset(clusterd+(mde.size_hi_minus_1+1)*SECTOR_SIZE,0,
576: (dblsb->s_sectperclust-mde.size_hi_minus_1-1)*
577: SECTOR_SIZE);
578: }
579:
580: return (mde.size_hi_minus_1+1)*SECTOR_SIZE;
581:
582: } /* end of read routine for fragmented cluster */
583: #endif
584:
585: if(mde.flags&1)
586: { /* cluster is not compressed */
587: for(i=0;i<nr_of_sectors;++i)
588: { bh=raw_bread(sb,sector+i);
589: if(bh==NULL)return -EIO;
590: memcpy(&clusterd[i*SECTOR_SIZE],bh->b_data,SECTOR_SIZE);
591: raw_brelse(sb,bh);
592: }
593: }
594: else
595: { /* cluster is compressed */
596:
597: membytes=SECTOR_SIZE*nr_of_sectors;
598:
599: clusterk=(unsigned char*)MALLOC(membytes);
600: if(clusterk==NULL)
601: { printk(KERN_ERR "DMSDOS: no memory for decompression!\n");
602: return -2;
603: }
604:
605: for(i=0;i<nr_of_sectors;++i)
606: { bh=raw_bread(sb,sector+i);
607: if(bh==NULL)
608: { FREE(clusterk);
609: return -EIO;
610: }
611: memcpy(&clusterk[i*SECTOR_SIZE],bh->b_data,SECTOR_SIZE);
612: raw_brelse(sb,bh);
613: }
614:
615: i=dbl_decompress(clusterd,clusterk,&mde);
616:
617: FREE(clusterk);
618:
619: if(i)
620: { printk(KERN_ERR "DMSDOS: decompression of cluster %d in CVF failed.\n",
621: clusternr);
622: return i;
623: }
624:
625: }
626:
627: /* the slack must be zerod out */
628: if(mde.size_hi_minus_1+1<dblsb->s_sectperclust)
629: { memset(clusterd+(mde.size_hi_minus_1+1)*SECTOR_SIZE,0,
630: (dblsb->s_sectperclust-mde.size_hi_minus_1-1)*
631: SECTOR_SIZE);
632: }
633:
634: return (mde.size_hi_minus_1+1)*SECTOR_SIZE;
635: }
636: #endif
637:
638: /* read a complete file cluster and decompress it if necessary;
639: it must be able to read directories
640: this function is unable to read cluster 0 (CVF root directory) */
641: /* returns cluster length in bytes or error (<0) */
642: /* this function is a generic wrapper */
643: int dmsdos_read_cluster(struct super_block*sb,
644: unsigned char*clusterd, int clusternr)
645: { int ret;
646: Dblsb*dblsb=MSDOS_SB(sb)->private_data;
647:
648: LOG_CLUST("DMSDOS: read_cluster %d\n",clusternr);
649:
650: switch(dblsb->s_cvf_version)
651: {
652: #ifdef DMSDOS_CONFIG_DBL
653: case DBLSP:
654: case DRVSP:
655: case DRVSP3:
656: ret=dbl_read_cluster(sb,clusterd,clusternr);
657: break;
658: #endif
659: #ifdef DMSDOS_CONFIG_STAC
660: case STAC3:
661: case STAC4:
662: ret=stac_read_cluster(sb,clusterd,clusternr);
663: break;
664: #endif
665: default:
666: printk(KERN_ERR "DMSDOS: read_cluster: illegal cvf version flag!\n");
667: ret=-EIO;
668: }
669:
670: return ret;
671: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.