/[pearpc]/src/io/prom/fs/hfsplus/volume.c
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Annotation of /src/io/prom/fs/hfsplus/volume.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1 - (hide annotations)
Wed Sep 5 17:11:21 2007 UTC (16 years, 7 months ago) by dpavlin
File MIME type: text/plain
File size: 18824 byte(s)
import upstream CVS
1 dpavlin 1 /*
2     * libhfs - library for reading and writing Macintosh HFS volumes
3     *
4     * Code to acces the basic volume information of a HFS+ volume.
5     *
6     * Copyright (C) 2000 Klaus Halfmann <klaus.halfmann@feri.de>
7     * Original work by 1996-1998 Robert Leslie <rob@mars.org>
8     * other work 2000 from Brad Boyer (flar@pants.nu)
9     * Additional work in 2004 by Stefan Weyergraf (stefan@weyergraf.de) for use in PearPC
10     *
11     * This program is free software; you can redistribute it and/or modify
12     * it under the terms of the GNU General Public License as published by
13     * the Free Software Foundation; either version 2 of the License, or
14     * (at your option) any later version.
15     *
16     * This program is distributed in the hope that it will be useful,
17     * but WITHOUT ANY WARRANTY; without even the implied warranty of
18     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19     * GNU General Public License for more details.
20     *
21     * You should have received a copy of the GNU General Public License
22     * along with this program; if not, write to the Free Software
23     * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24     *
25     */
26    
27     # ifdef HAVE_CONFIG_H
28     # include "config.h"
29     # endif
30    
31     # include <stdlib.h>
32     # include <stdio.h>
33     # include <string.h>
34     # include <time.h>
35     # include <errno.h>
36    
37     # include "libhfsp.h"
38     # include "volume.h"
39     # include "record.h"
40     # include "btree.h"
41     # include "blockiter.h"
42     # include "os.h"
43     # include "swab.h"
44     # include "hfstime.h"
45     # include "partitions.h"
46    
47     /*
48     * A replacement of the ffs C library function which does not exist
49     * in all C libraries.
50     *
51     * ffs finds the first set bit in a four-byte integer
52     */
53     static unsigned long
54     my_ffs( unsigned long i )
55     {
56     register unsigned long j;
57    
58     if( i == 0 ) return( 0 );
59    
60     for( j = 1; j <= ( sizeof( unsigned long )*8 ); j++ )
61     {
62     if( (i>>(j-1))&1 ) return( j );
63     }
64    
65     return( 0 );
66     }
67    
68     #define ffs(_a_) my_ffs(_a_)
69    
70     /* Fill a given buffer with the given block in volume.
71     */
72     int volume_readinbuf(volume * vol,void* buf, long block)
73     {
74     UInt16 blksize_bits;
75     ASSERT( block < vol->maxblocks);
76    
77     blksize_bits = vol->blksize_bits;
78     // printf("Reading from %lx\n", block << blksize_bits);
79     if (hfsp_os_seek(&vol->fd, block, blksize_bits) == block)
80     if (1 == hfsp_os_read(&vol->fd, buf, 1, blksize_bits))
81     return 0;
82     return -1;
83     }
84    
85     /* Write buffer to given block on medium.
86     */
87     int volume_writetobuf(volume * vol,void* buf, long block)
88     {
89     UInt16 blksize_bits;
90     ASSERT( block < vol->maxblocks);
91    
92     blksize_bits = vol->blksize_bits;
93     // printf("Writing to %lx\n", block << blksize_bits);
94     if (hfsp_os_seek(&vol->fd, block, blksize_bits) == block)
95     if (1 == hfsp_os_write(&vol->fd, buf, 1, blksize_bits))
96     return 0;
97     return -1;
98     }
99    
100     /* read multiple blocks of a fork into given memory.
101     *
102     * block realtive index in fork to start with
103     * count number of blocks to read
104     * forktype usually HFSP_EXTENT_DATA or HFSP_EXTENT_RSRC
105     * fileId id (needed) in case extents must be fetched
106     *
107     * returns given pinter or NULL on failure.
108     */
109     void* volume_readfromfork(volume* vol, void* buf,
110     hfsp_fork_raw* f, UInt32 block,
111     UInt32 count, UInt8 forktype, UInt32 fileId)
112     {
113     blockiter iter;
114     char* cbuf = buf;
115    
116     blockiter_init(&iter, vol, f, forktype, fileId);
117     if (blockiter_skip(&iter, block))
118     return NULL;
119     while (count > 0)
120     {
121     --count;
122     if (volume_readinbuf(vol, cbuf, blockiter_curr(&iter)))
123     return NULL;
124     cbuf += vol->blksize;
125     if (count > 0 && blockiter_next(&iter))
126     return NULL;
127     }
128     return buf;
129     }
130    
131     /* write multiple blocks of a fork buf to medium.
132     * The caller is responsible for allocating a suffient
133     * large fork and eventually needed extends records for now.
134     *
135     * block realtive index in fork to start with
136     * count number of blocks to write
137     * forktype usually HFSP_EXTENT_DATA or HFSP_EXTENT_RSRC
138     * fileId id (needed) in case extents must be written
139     *
140     * returns value != 0 on error.
141     */
142     int volume_writetofork(volume* vol, void* buf,
143     hfsp_fork_raw* f, UInt32 block,
144     UInt32 count, UInt8 forktype, UInt32 fileId)
145     {
146     blockiter iter;
147     char* cbuf = buf;
148    
149     blockiter_init(&iter, vol, f, forktype, fileId);
150     if (blockiter_skip(&iter, block))
151     return -1;
152     while (count > 0)
153     {
154     --count;
155     if (volume_writetobuf(vol, cbuf, blockiter_curr(&iter)))
156     return -1;
157     cbuf += vol->blksize;
158     if (count > 0 && blockiter_next(&iter))
159     return -1;
160     }
161     return 0;
162     }
163    
164     /* Check in Allocation file if given block is allocated.
165     */
166     int volume_allocated(volume* vol, UInt32 block)
167     {
168     int bit = block & 0x07;
169     int mask,index;
170     char* bits;
171     char buf[vol->blksize];
172    
173     // if (block >= vol->maxblocks)
174     // HFSP_ERROR(-1, "Allocation block out of range.");
175     block >>= 3;
176     mask = (1 << vol->blksize_bits) -1; /* Usually 0x0FFF */
177     index = block & mask;
178     block >>= vol->blksize_bits; // block in allocation file
179     bits = (char*) volume_readfromfork(vol, buf, &vol->vol.alloc_file,
180     block, 1, HFSP_EXTENT_DATA, HFSP_ALLOC_CNID);
181     if (!bits)
182     HFSP_ERROR(-1, "Allocation block not found !?");
183    
184     return (bits[index] & (0x80 >> bit)); /* Bit one is 0x80 ! */
185     return 0;
186     fail:
187     return -1;
188     }
189    
190     /* Mark in Allocation file a given block as allocated.
191     *
192     * ToDo: optimize for adjacent blocks ...
193     * use cache directly
194     */
195     int volume_allocate(volume* vol, UInt32 block)
196     {
197     int bit = block & 0x07;
198     int mask,index;
199     char* bits;
200     char buf[vol->blksize];
201     int shift = 0x80 >> bit; /* Bit one is 0x80 */
202    
203     // if (block >= vol->maxblocks)
204     // HFSP_ERROR(-1, "Allocation block out of range.");
205     block >>= 3;
206     mask = (1 << vol->blksize_bits) -1; /* Usually 0x0FFF */
207     index = block & mask;
208     block >>= vol->blksize_bits; // block in allocation file
209     bits = (char*) volume_readfromfork(vol, buf, &vol->vol.alloc_file,
210     block, 1, HFSP_EXTENT_DATA, HFSP_ALLOC_CNID);
211     if (!bits)
212     HFSP_ERROR(-1, "Allocation block not found !?");
213    
214     if (bits[index] & shift)
215     HFSP_ERROR(-1, "volume_allocate: Block already allocated");
216     bits[index] |= shift;
217     return volume_writetofork(vol, buf, &vol->vol.alloc_file,
218     block, 1, HFSP_EXTENT_DATA, HFSP_ALLOC_CNID);
219     fail:
220     return -1;
221     }
222    
223     /* Mark in Allocation file a given block as freee.
224     *
225     * ToDo: optimize for adjacent blocks ...
226     * use cache directly
227     */
228     int volume_deallocate(volume* vol, UInt32 block)
229     {
230     int bit = block & 0x07;
231     int mask,index;
232     char* bits;
233     char buf[vol->blksize];
234     int shift = 0x80 >> bit; /* Bit one is 0x80 */
235    
236     // if (block >= vol->maxblocks)
237     // HFSP_ERROR(-1, "Allocation block out of range.");
238     block >>= 3;
239     mask = (1 << vol->blksize_bits) -1; /* Usually 0x0FFF */
240     index = block & mask;
241     block >>= vol->blksize_bits; // block in allocation file
242     bits = (char*) volume_readfromfork(vol, buf, &vol->vol.alloc_file,
243     block, 1, HFSP_EXTENT_DATA, HFSP_ALLOC_CNID);
244     if (!bits)
245     HFSP_ERROR(-1, "Allocation block not found !?");
246    
247     if (!(bits[index] & shift))
248     HFSP_ERROR(-1, "volume_allocate: Block already free");
249     bits[index] &= ~shift;
250     return volume_writetofork(vol, buf, &vol->vol.alloc_file,
251     block, 1, HFSP_EXTENT_DATA, HFSP_ALLOC_CNID);
252     fail:
253     return -1;
254     }
255    
256     /* Initialize a raw hfsp_extent_rec.
257     */
258     static void volume_initextent(hfsp_extent_rec er)
259     {
260     memset(er, 0, 8 * sizeof(hfsp_extent));
261     /*
262     int i;
263     hfsp_extent* e;
264     for (i=0; i < 8; i++)
265     {
266     e = &er[i];
267     e->start_block = 0;
268     e->block_count = 0;
269     }
270     */
271     }
272    
273     /** Initalize an (empty !) fork, you may later request additional space
274     */
275    
276     void volume_initfork(volume* vol, hfsp_fork_raw* f, UInt16 fork_type)
277     {
278     f->total_size = 0;
279     if (fork_type == HFSP_EXTENT_DATA)
280     f->clump_size = vol->vol.data_clump_sz;
281     else
282     f->clump_size = vol->vol.rsrc_clump_sz;
283     f->total_blocks = 0;
284     volume_initextent(f->extents);
285     }
286    
287     /* Read a raw hfsp_extent_rec from memory.
288     *
289     * return pointer right after the structure.
290     */
291     char* volume_readextent(char *p, hfsp_extent_rec er)
292     {
293     int i;
294     hfsp_extent* e;
295     for (i=0; i < 8; i++)
296     {
297     e = &er[i];
298     e->start_block = bswabU32_inc(&p);
299     e->block_count = bswabU32_inc(&p);
300     }
301     return p;
302     }
303    
304     /* Write a raw hfsp_extent_rec to memory.
305     *
306     * return pointer right after the structure.
307     */
308     char* volume_writeextent(char *p, hfsp_extent_rec er)
309     {
310     int i;
311     hfsp_extent* e;
312     for (i=0; i < 8; i++)
313     {
314     e = &er[i];
315     bstoreU32_inc(&p, e->start_block );
316     bstoreU32_inc(&p, e->block_count );
317     }
318     return p;
319     }
320    
321     /* Read a raw hfsp_fork from memory.
322     *
323     * return pointer right after the structure.
324     */
325     char* volume_readfork(char *p, hfsp_fork_raw* f)
326     {
327     f->total_size = bswabU64_inc(&p);
328     f->clump_size = bswabU32_inc(&p);
329     f->total_blocks = bswabU32_inc(&p);
330     return volume_readextent(p, f->extents);
331     }
332    
333     /* Write a raw hfsp_fork to memory.
334     *
335     * return pointer right after the structure.
336     */
337     char* volume_writefork(char *p, hfsp_fork_raw* f)
338     {
339     bstoreU64_inc(&p, f->total_size );
340     bstoreU32_inc(&p, f->clump_size );
341     bstoreU32_inc(&p, f->total_blocks);
342     return volume_writeextent(p, f->extents);
343     }
344    
345     /* Read the volume from the given buffer and swap the bytes.
346     */
347     static int volume_readbuf(hfsp_vh* vh, char* p)
348     {
349     if ( (vh->signature = bswabU16_inc(&p)) != HFSP_VOLHEAD_SIG)
350     HFSP_ERROR(-1, "This is not a HFS+ volume");
351     vh->version = bswabU16_inc(&p);
352     vh->attributes = bswabU32_inc(&p);
353     vh->last_mount_vers = bswabU32_inc(&p);
354     vh->reserved = bswabU32_inc(&p);
355     vh->create_date = bswabU32_inc(&p);
356     vh->modify_date = bswabU32_inc(&p);
357     vh->backup_date = bswabU32_inc(&p);
358     vh->checked_date = bswabU32_inc(&p);
359     vh->file_count = bswabU32_inc(&p);
360     vh->folder_count = bswabU32_inc(&p);
361     vh->blocksize = bswabU32_inc(&p);
362     vh->total_blocks = bswabU32_inc(&p);
363     vh->free_blocks = bswabU32_inc(&p);
364     vh->next_alloc = bswabU32_inc(&p);
365     vh->rsrc_clump_sz = bswabU32_inc(&p);
366     vh->data_clump_sz = bswabU32_inc(&p);
367     vh->next_cnid = bswabU32_inc(&p);
368     vh->write_count = bswabU32_inc(&p);
369     vh->encodings_bmp = bswabU64_inc(&p);
370     memcpy(vh->finder_info, p, 32);
371     p += 32; // finderinfo is not used by now
372     p = volume_readfork(p, &vh->alloc_file );
373     p = volume_readfork(p, &vh->ext_file );
374     p = volume_readfork(p, &vh->cat_file );
375     p = volume_readfork(p, &vh->attr_file );
376     p = volume_readfork(p, &vh->start_file );
377     return 0;
378     fail:
379     return -1;
380     }
381    
382     /* Write the volume to the given buffer and swap the bytes.
383     */
384     static int volume_writebuf(hfsp_vh* vh, char* p)
385     {
386     bstoreU16_inc(&p, vh->signature );
387     bstoreU16_inc(&p, vh->version );
388     bstoreU32_inc(&p, vh->attributes );
389     bstoreU32_inc(&p, vh->last_mount_vers);
390     bstoreU32_inc(&p, vh->reserved );
391     bstoreU32_inc(&p, vh->create_date );
392     bstoreU32_inc(&p, vh->modify_date );
393     bstoreU32_inc(&p, vh->backup_date );
394     bstoreU32_inc(&p, vh->checked_date );
395     bstoreU32_inc(&p, vh->file_count );
396     bstoreU32_inc(&p, vh->folder_count );
397     bstoreU32_inc(&p, vh->blocksize );
398     bstoreU32_inc(&p, vh->total_blocks );
399     bstoreU32_inc(&p, vh->free_blocks );
400     bstoreU32_inc(&p, vh->next_alloc );
401     bstoreU32_inc(&p, vh->rsrc_clump_sz );
402     bstoreU32_inc(&p, vh->data_clump_sz );
403     bstoreU32_inc(&p, vh->next_cnid );
404     bstoreU32_inc(&p, vh->write_count );
405     bstoreU64_inc(&p, vh->encodings_bmp );
406     memcpy(p, vh->finder_info, 32);
407     p += 32; // finderinfo is not used by now
408     p = volume_writefork(p, &vh->alloc_file );
409     p = volume_writefork(p, &vh->ext_file );
410     p = volume_writefork(p, &vh->cat_file );
411     p = volume_writefork(p, &vh->attr_file );
412     p = volume_writefork(p, &vh->start_file );
413     return 0;
414     }
415    
416     /* Read the volume from the given block */
417     static int volume_read(volume * vol, hfsp_vh* vh, UInt32 block)
418     {
419     char buf[vol->blksize];
420     if (volume_readinbuf(vol, buf, block))
421     return -1;
422     return volume_readbuf(vh, buf);
423     }
424    
425     /* Find out whether the volume is wrapped and unwrap it eventually */
426     static int volume_read_wrapper(volume * vol, hfsp_vh* vh)
427     {
428     UInt16 signature;
429     char buf[vol->blksize];
430     char *p = buf;
431     if( volume_readinbuf(vol, buf, 2) ) // Wrapper or volume header starts here
432     return -1;
433    
434     signature = bswabU16_inc(&p);
435     if (signature == HFS_VOLHEAD_SIG) /* Wrapper */
436     {
437     UInt32 drAlBlkSiz; /* size (in bytes) of allocation blocks */
438     UInt32 sect_per_block; /* how may block build an hfs sector */
439     UInt16 drAlBlSt; /* first allocation block in volume */
440    
441     UInt16 embeds, embedl; /* Start/lenght of embedded area in blocks */
442    
443     p += 0x12; /* skip unneeded HFS vol fields */
444     drAlBlkSiz = bswabU32_inc(&p); /* offset 0x14 */
445     p += 0x4; /* skip unneeded HFS vol fields */
446     drAlBlSt = bswabU16_inc(&p); /* offset 0x1C */
447    
448     p += 0x5E; /* skip unneeded HFS vol fields */
449     signature = bswabU16_inc(&p); /* offset 0x7C, drEmbedSigWord */
450     if (signature != HFSP_VOLHEAD_SIG)
451     HFSP_ERROR(-1, "This looks like a normal HFS volume");
452     embeds = bswabU16_inc(&p);
453     embedl = bswabU16_inc(&p);
454     sect_per_block = (drAlBlkSiz / HFSP_BLOCKSZ);
455     // end is absolute (not relative to HFS+ start)
456     vol->maxblocks = embedl * sect_per_block;
457     hfsp_set_offset(&vol->fd, hfsp_get_offset(&vol->fd)
458     + (((APPLEUInt64) (drAlBlSt + embeds * sect_per_block)) << HFS_BLOCKSZ_BITS));
459     /* Now we can try to read the embedded HFS+ volume header */
460     return volume_read(vol,vh,2);
461     }
462     else if (signature == HFSP_VOLHEAD_SIG) /* Native HFS+ volume */
463     {
464     p = buf; // Restore to begin of block
465     return volume_readbuf(vh, p);
466     } else
467     HFSP_ERROR(-1, "Neither Wrapper nor native HFS+ volume header found");
468    
469     fail:
470     return -1;
471     }
472    
473     /** Mark the volume as modified by setting its modfied date */
474     void volume_modified(volume* vol)
475     {
476     time_t now;
477     hfsp_vh* head;
478    
479     gmtime(&now);
480     head = &vol->vol;
481     head->modify_date = now + HFSPTIMEDIFF;
482     }
483    
484     /** Mark this volume as used by Linux by modifying the header */
485     void volume_linux_mark(volume* vol)
486     {
487     hfsp_vh* head = &vol->vol;
488    
489     // *** Debugging ***
490     vol ->flags |= HFSP_BACKUP_DIRTY;
491    
492     // MacOS does not like that :(
493     // head->version = HPUTILS_VERS;
494     head->last_mount_vers = HPLS_SIGNATURE;
495     // For now I always mark the volume as inconsistent ...
496     head->attributes |= HFSPLUS_VOL_INCNSTNT;
497     volume_modified(vol);
498     }
499    
500     #include <stdio.h>
501     /* Open the device, read and verify the volume header
502     (and its backup) */
503     int volume_open(volume* vol, const void *devicehandle, int partition, int rw)
504     {
505     hfsp_vh backup; /* backup volume found at second to last block */
506     int shift;
507     int blksize_bits;
508    
509     vol->blksize_bits = HFSP_BLOCKSZ_BITS;
510     vol->flags = 0;
511     vol->blksize = HFSP_BLOCKSZ;
512     vol->maxblocks = 3;
513     /* this should be enough until we find the volume descriptor */
514     vol->extents = NULL; /* Thanks to Jeremias Sauceda */
515    
516     btree_reset(&vol->catalog);
517    
518     if (hfsp_os_open(&vol->fd, devicehandle, rw))
519     return -1;
520    
521     hfsp_set_offset(&vol->fd, 0);
522    
523     /* set the offset to the first block of the given partition */
524     if (partition != 0) {
525     partition_map map;
526     int block;
527     if( partition_getPartitionMap( &map, vol->fd)== -1)
528     HFSP_ERROR(-1, "No Apple partition map found");
529     block = partition_getStartBlock( &map, "Apple_HFS", partition);
530     if (block == 0)
531     HFSP_ERROR(-1, "No valid Apple_HFS partition found");
532     hfsp_set_offset(&vol->fd, ((APPLEUInt64)block) << HFSP_BLOCKSZ_BITS);
533     }
534    
535     vol->flags |= rw & HFSP_MODE_RDWR;
536    
537     if (volume_read_wrapper(vol, &vol->vol))
538     return -1;
539    
540     if (volume_read(vol, &backup, vol->maxblocks - 2))
541     /*return -1*/; /* ignore lacking backup */
542     // fprintf(stderr, "HFS+ has no backup volume header. skipping...\n");
543    
544     /* Now switch blksize from HFSP_BLOCKSZ (512) to value given in header
545     and adjust depend values accordingly, after that a block always
546     means a HFS+ allocation size */
547    
548     /* Usually blocksize is 4096 */
549     blksize_bits = ffs(vol->vol.blocksize) -1;
550     shift = blksize_bits - vol->blksize_bits;
551     vol -> blksize = vol->vol.blocksize;
552     vol -> blksize_bits = blksize_bits;
553     vol -> maxblocks = vol->vol.total_blocks; /* cant calculate via shift ? */
554    
555     if (vol->flags & HFSP_MODE_RDWR)
556     {
557     char buf[HFSP_BLOCKSZ];
558     void *p = buf;
559    
560     volume_linux_mark(vol);
561    
562     // write back (dirty) volume header
563     if (volume_writebuf(&vol->vol, p))
564     return -1; // evil, but will never happen
565    
566     volume_writetobuf(vol, buf, 2); // This is always block 2
567     }
568    
569     if (btree_init_cat(&vol->catalog, vol, &vol->vol.cat_file))
570     return -1;
571    
572     return 0;
573     fail:
574     return -1;
575     }
576    
577     /* Write back all data eventually cached and close the device */
578     int volume_close(volume* vol)
579     {
580     btree_close(&vol->catalog);
581     if (vol->extents)
582     {
583     btree_close(vol->extents);
584     FREE(vol->extents);
585     }
586     if (vol->flags & HFSP_MODE_RDWR) // volume was opened for writing,
587     {
588     /* Switch back to HFSP_BLOCKSZ (512) */
589     int shift = vol->blksize_bits - HFSP_BLOCKSZ_BITS;
590     char buf[HFSP_BLOCKSZ];
591     void *p = buf;
592    
593     hfsp_vh* head = &vol->vol;
594    
595     // Clear inconsistent flag
596     head->attributes &= ~HFSPLUS_VOL_INCNSTNT;
597     // set Unmounted flag
598     head->attributes |= HFSPLUS_VOL_UNMNT;
599    
600     vol->blksize_bits = HFSP_BLOCKSZ_BITS;
601    
602     vol -> maxblocks <<= shift; /* cant calculate via shift ? */
603    
604     if (volume_writebuf(&vol->vol, p))
605     return -1; // evil, but will never happen
606    
607     volume_writetobuf(vol, buf, 2); // This is always block 2
608    
609     if (vol->flags & HFSP_BACKUP_DIRTY) // need to write backup block, too
610     {
611     // !!! Need to check this with larger volumes, too !!!
612     volume_writetobuf(vol, buf, vol->maxblocks-2);
613     }
614     }
615     return hfsp_os_close(&vol->fd);
616     }
617    
618     /* internal fucntion used to create the extents btree,
619     is called by inline function when needed */
620     void volume_create_extents_tree(volume* vol)
621     {
622     btree* result = (btree*) ALLOC(btree*, sizeof(btree));
623     if (!result)
624     HFSP_ERROR(ENOMEM, "No memory for extents btree");
625     if (!btree_init_extent(result, vol, &vol->vol.ext_file))
626     {
627     vol->extents = result;
628     return;
629     }
630     fail:
631     vol->extents = NULL;
632     }
633    
634     /* return new Id for files/folder and check for overflow.
635     *
636     * retun 0 on error .
637     */
638     UInt32 volume_get_nextid(volume* vol)
639     {
640     UInt32 result = vol->vol.next_cnid;
641     if (result < HFSP_MIN_CNID) // oops possible wrap around overflow
642     {
643     hfsp_error = "Maximum number of node IDs exhausted, sorry";
644     return 0;
645     }
646     vol->vol.next_cnid = 1 + result;
647     return result;
648     }

  ViewVC Help
Powered by ViewVC 1.1.26