/[rdesktop]/sourceforge.net/trunk/rdesktop/disk.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

Contents of /sourceforge.net/trunk/rdesktop/disk.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 597 - (show annotations)
Thu Feb 5 15:41:56 2004 UTC (20 years, 3 months ago) by n-ki
File MIME type: text/plain
File size: 17349 byte(s)
add some defines. handle dirs first, otherwise we can not create dirs

1 /* -*- c-basic-offset: 8 -*-
2 rdesktop: A Remote Desktop Protocol client.
3 Disk Redirection
4 Copyright (C) Jeroen Meijer 2003
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 */
20
21 #define FILE_ATTRIBUTE_READONLY 0x00000001
22 #define FILE_ATTRIBUTE_HIDDEN 0x00000002
23 #define FILE_ATTRIBUTE_SYSTEM 0x00000004
24 #define FILE_ATTRIBUTE_DIRECTORY 0x00000010
25 #define FILE_ATTRIBUTE_ARCHIVE 0x00000020
26 #define FILE_ATTRIBUTE_DEVICE 0x00000040
27 #define FILE_ATTRIBUTE_UNKNOWNXXX0 0x00000060 /* ??? ACTION i.e. 0x860 == compress this file ? */
28 #define FILE_ATTRIBUTE_NORMAL 0x00000080
29 #define FILE_ATTRIBUTE_TEMPORARY 0x00000100
30 #define FILE_ATTRIBUTE_SPARSE_FILE 0x00000200
31 #define FILE_ATTRIBUTE_REPARSE_POINT 0x00000400
32 #define FILE_ATTRIBUTE_COMPRESSED 0x00000800
33 #define FILE_ATTRIBUTE_OFFLINE 0x00001000
34 #define FILE_ATTRIBUTE_NOT_CONTENT_INDEXED 0x00002000
35 #define FILE_ATTRIBUTE_ENCRYPTED 0x00004000
36
37 #define FILE_FLAG_OPEN_NO_RECALL 0x00100000
38 #define FILE_FLAG_OPEN_REPARSE_POINT 0x00200000
39 #define FILE_FLAG_POSIX_SEMANTICS 0x01000000
40 #define FILE_FLAG_BACKUP_SEMANTICS 0x02000000 /* sometimes used to create a directory */
41 #define FILE_FLAG_DELETE_ON_CLOSE 0x04000000
42 #define FILE_FLAG_SEQUENTIAL_SCAN 0x08000000
43 #define FILE_FLAG_RANDOM_ACCESS 0x10000000
44 #define FILE_FLAG_NO_BUFFERING 0x20000000
45 #define FILE_FLAG_OVERLAPPED 0x40000000
46 #define FILE_FLAG_WRITE_THROUGH 0x80000000
47
48 #define FILE_SHARE_READ 0x01
49 #define FILE_SHARE_WRITE 0x02
50 #define FILE_SHARE_DELETE 0x04
51
52 #define FILE_BASIC_INFORMATION 0x04
53 #define FILE_STANDARD_INFORMATION 0x05
54
55 #define FS_CASE_SENSITIVE 0x00000001
56 #define FS_CASE_IS_PRESERVED 0x00000002
57 #define FS_UNICODE_STORED_ON_DISK 0x00000004
58 #define FS_PERSISTENT_ACLS 0x00000008
59 #define FS_FILE_COMPRESSION 0x00000010
60 #define FS_VOLUME_QUOTAS 0x00000020
61 #define FS_SUPPORTS_SPARSE_FILES 0x00000040
62 #define FS_SUPPORTS_REPARSE_POINTS 0x00000080
63 #define FS_SUPPORTS_REMOTE_STORAGE 0X00000100
64 #define FS_VOL_IS_COMPRESSED 0x00008000
65 #define FILE_READ_ONLY_VOLUME 0x00080000
66
67 #define OPEN_EXISTING 1
68 #define CREATE_NEW 2
69 #define OPEN_ALWAYS 3
70 #define TRUNCATE_EXISTING 4
71 #define CREATE_ALWAYS 5
72
73 #define GENERIC_READ 0x80000000
74 #define GENERIC_WRITE 0x40000000
75 #define GENERIC_EXECUTE 0x20000000
76 #define GENERIC_ALL 0x10000000
77
78 #define ERROR_FILE_NOT_FOUND 2L
79 #define ERROR_ALREADY_EXISTS 183L
80
81 #define MAX_OPEN_FILES 0x100
82
83 #if (defined(sun) && (defined(__svr4__) || defined(__SVR4)))
84 #define SOLARIS
85 #endif
86
87 #ifdef SOLARIS
88 #define DIRFD(a) ((a)->dd_fd)
89 #else
90 #define DIRFD(a) (dirfd(a))
91 #endif
92
93 #include <sys/types.h>
94 #include <sys/stat.h>
95 #include <unistd.h>
96 #include <fcntl.h> /* open, close */
97 #include <dirent.h> /* opendir, closedir, readdir */
98 #include <fnmatch.h>
99 #include <errno.h> /* errno */
100
101 #if defined(SOLARIS)
102 #include <sys/statvfs.h> /* solaris statvfs */
103 #define STATFS_FN(path, buf) (statvfs(path,buf))
104 #define STATFS_T statvfs
105 #define F_NAMELEN(buf) ((buf).f_namemax)
106
107 #elif (defined(__OpenBSD__) || defined(__NetBSD__) || defined(__FreeBSD__))
108 #include <sys/param.h>
109 #include <sys/mount.h>
110 #define STATFS_FN(path, buf) (statfs(path,buf))
111 #define STATFS_T statfs
112 #define F_NAMELEN(buf) (NAME_MAX)
113
114 #else
115 #include <sys/vfs.h> /* linux statfs */
116 #define STATFS_FN(path, buf) (statfs(path,buf))
117 #define STATFS_T statfs
118 #define F_NAMELEN(buf) ((buf).f_namelen)
119 #endif
120
121 #include "rdesktop.h"
122
123 extern RDPDR_DEVICE g_rdpdr_device[];
124
125 struct fileinfo
126 {
127 uint32 device_id, flags_and_attributes;
128 char path[256];
129 DIR *pdir;
130 struct dirent *pdirent;
131 char pattern[64];
132 BOOL delete_on_close;
133 }
134 g_fileinfo[MAX_OPEN_FILES];
135
136 /* Convert seconds since 1970 to a filetime */
137 void
138 seconds_since_1970_to_filetime(time_t seconds, uint32 * high, uint32 * low)
139 {
140 unsigned long long ticks;
141
142 ticks = (seconds + 11644473600LL) * 10000000;
143 *low = (uint32) ticks;
144 *high = (uint32) (ticks >> 32);
145 }
146
147 /* Enumeration of devices from rdesktop.c */
148 /* returns numer of units found and initialized. */
149 /* optarg looks like ':h:=/mnt/floppy,b:=/mnt/usbdevice1' */
150 /* when it arrives to this function. */
151 int
152 disk_enum_devices(int *id, char *optarg)
153 {
154 char *pos = optarg;
155 char *pos2;
156 int count = 0;
157
158 // skip the first colon
159 optarg++;
160 while ((pos = next_arg(optarg, ',')) && *id < RDPDR_MAX_DEVICES)
161 {
162 pos2 = next_arg(optarg, '=');
163 strcpy(g_rdpdr_device[*id].name, optarg);
164
165 toupper_str(g_rdpdr_device[*id].name);
166
167 /* add trailing colon to name. */
168 strcat(g_rdpdr_device[*id].name, ":");
169
170 g_rdpdr_device[*id].local_path = xmalloc(strlen(pos2) + 1);
171 strcpy(g_rdpdr_device[*id].local_path, pos2);
172 printf("DISK %s to %s\n", g_rdpdr_device[*id].name, g_rdpdr_device[*id].local_path);
173 g_rdpdr_device[*id].device_type = DEVICE_TYPE_DISK;
174 count++;
175 (*id)++;
176
177 optarg = pos;
178 }
179 return count;
180 }
181
182 /* Opens or creates a file or directory */
183 NTSTATUS
184 disk_create(uint32 device_id, uint32 accessmask, uint32 sharemode, uint32 create_disposition,
185 uint32 flags_and_attributes, char *filename, HANDLE * phandle)
186 {
187 HANDLE handle;
188 DIR *dirp;
189 int flags, mode;
190 char path[256];
191
192 handle = 0;
193 dirp = NULL;
194 flags = 0;
195 mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
196
197 if (filename[strlen(filename) - 1] == '/')
198 filename[strlen(filename) - 1] = 0;
199 sprintf(path, "%s%s", g_rdpdr_device[device_id].local_path, filename);
200
201 switch (create_disposition)
202 {
203 case CREATE_ALWAYS:
204
205 // Delete existing file/link.
206 unlink(path);
207 flags |= O_CREAT;
208 break;
209
210 case CREATE_NEW:
211
212 // If the file already exists, then fail.
213 flags |= O_CREAT | O_EXCL;
214 break;
215
216 case OPEN_ALWAYS:
217
218 // Create if not already exists.
219 flags |= O_CREAT;
220 break;
221
222 case OPEN_EXISTING:
223
224 // Default behaviour
225 break;
226
227 case TRUNCATE_EXISTING:
228
229 // If the file does not exist, then fail.
230 flags |= O_TRUNC;
231 break;
232 }
233
234 //printf("Open: \"%s\" flags: %u, accessmask: %u sharemode: %u create disp: %u\n", path, flags_and_attributes, accessmask, sharemode, create_disposition);
235
236 /* since we can't trust the FILE_DIRECTORY_FILE flag */
237 /* we need to double check that the file isn't a dir */
238 struct stat filestat;
239
240 // Get information about file and set that flag ourselfs
241 if ((stat(path, &filestat) == 0) && (S_ISDIR(filestat.st_mode)))
242 flags_and_attributes |= FILE_DIRECTORY_FILE;
243
244 if (flags_and_attributes & FILE_DIRECTORY_FILE)
245 {
246 if (flags & O_CREAT)
247 {
248 mkdir(path, mode);
249 }
250
251 dirp = opendir(path);
252 if (!dirp)
253 {
254 switch (errno)
255 {
256 case EACCES:
257
258 return STATUS_ACCESS_DENIED;
259
260 case ENOENT:
261
262 return STATUS_NO_SUCH_FILE;
263
264 default:
265
266 perror("opendir");
267 return STATUS_NO_SUCH_FILE;
268 }
269 }
270 handle = DIRFD(dirp);
271 }
272 else
273 {
274
275 if (accessmask & GENERIC_ALL
276 || (accessmask & GENERIC_READ && accessmask & GENERIC_WRITE))
277 {
278 flags |= O_RDWR;
279 }
280 else if ((accessmask & GENERIC_WRITE) && !(accessmask & GENERIC_READ))
281 {
282 flags |= O_WRONLY;
283 }
284 else
285 {
286 flags |= O_RDONLY;
287 }
288
289 handle = open(path, flags, mode);
290 if (handle == -1)
291 {
292 switch (errno)
293 {
294 case EACCES:
295
296 return STATUS_ACCESS_DENIED;
297
298 case ENOENT:
299
300 return STATUS_NO_SUCH_FILE;
301 default:
302
303 perror("open");
304 return STATUS_NO_SUCH_FILE;
305 }
306 }
307
308 /* all read and writes of files should be non blocking */
309 if (fcntl(handle, F_SETFL, O_NONBLOCK) == -1)
310 perror("fcntl");
311 }
312
313 if (handle >= MAX_OPEN_FILES)
314 {
315 error("Maximum number of open files (%s) reached. Increase MAX_OPEN_FILES!\n",
316 handle);
317 exit(1);
318 }
319
320 if (dirp)
321 g_fileinfo[handle].pdir = dirp;
322 g_fileinfo[handle].device_id = device_id;
323 g_fileinfo[handle].flags_and_attributes = flags_and_attributes;
324 strncpy(g_fileinfo[handle].path, path, 255);
325
326 *phandle = handle;
327 return STATUS_SUCCESS;
328 }
329
330 NTSTATUS
331 disk_close(HANDLE handle)
332 {
333 struct fileinfo *pfinfo;
334
335 pfinfo = &(g_fileinfo[handle]);
336
337 if (pfinfo->flags_and_attributes & FILE_DIRECTORY_FILE)
338 {
339 closedir(pfinfo->pdir);
340 //FIXME: Should check exit code
341 }
342 else
343 {
344 close(handle);
345 }
346
347 return STATUS_SUCCESS;
348 }
349
350 NTSTATUS
351 disk_read(HANDLE handle, uint8 * data, uint32 length, uint32 offset, uint32 * result)
352 {
353 int n;
354
355 /* browsing dir ???? */
356 /* each request is 24 bytes */
357 if (g_fileinfo[handle].flags_and_attributes & FILE_DIRECTORY_FILE)
358 {
359 *result = 0;
360 return STATUS_SUCCESS;
361 }
362
363 if (offset)
364 lseek(handle, offset, SEEK_SET);
365 n = read(handle, data, length);
366
367 if (n < 0)
368 {
369 perror("read");
370 *result = 0;
371 return STATUS_INVALID_PARAMETER;
372 }
373
374 *result = n;
375
376 return STATUS_SUCCESS;
377 }
378
379 NTSTATUS
380 disk_write(HANDLE handle, uint8 * data, uint32 length, uint32 offset, uint32 * result)
381 {
382 int n;
383
384 if (offset)
385 lseek(handle, offset, SEEK_SET);
386
387 n = write(handle, data, length);
388
389 if (n < 0)
390 {
391 perror("write");
392 *result = 0;
393 return STATUS_ACCESS_DENIED;
394 }
395
396 *result = n;
397
398 return STATUS_SUCCESS;
399 }
400
401 NTSTATUS
402 disk_query_information(HANDLE handle, uint32 info_class, STREAM out)
403 {
404 uint32 file_attributes, ft_high, ft_low;
405 struct stat filestat;
406 char *path, *filename;
407
408 path = g_fileinfo[handle].path;
409
410 // Get information about file
411 if (fstat(handle, &filestat) != 0)
412 {
413 perror("stat");
414 out_uint8(out, 0);
415 return STATUS_ACCESS_DENIED;
416 }
417
418 // Set file attributes
419 file_attributes = 0;
420 if (S_ISDIR(filestat.st_mode))
421 {
422 file_attributes |= FILE_ATTRIBUTE_DIRECTORY;
423 }
424 filename = 1 + strrchr(path, '/');
425 if (filename && filename[0] == '.')
426 {
427 file_attributes |= FILE_ATTRIBUTE_HIDDEN;
428 }
429
430 // Return requested data
431 switch (info_class)
432 {
433 case 4: /* FileBasicInformation */
434
435 out_uint8s(out, 8); //create_time not available;
436
437 seconds_since_1970_to_filetime(filestat.st_atime, &ft_high, &ft_low);
438 out_uint32_le(out, ft_low); //last_access_time
439 out_uint32_le(out, ft_high);
440
441 seconds_since_1970_to_filetime(filestat.st_mtime, &ft_high, &ft_low);
442 out_uint32_le(out, ft_low); //last_write_time
443 out_uint32_le(out, ft_high);
444
445 out_uint8s(out, 8); //unknown zero
446 out_uint32_le(out, file_attributes);
447 break;
448
449 case 5: /* FileStandardInformation */
450
451 out_uint32_le(out, filestat.st_size); //Allocation size
452 out_uint32_le(out, 0);
453 out_uint32_le(out, filestat.st_size); //End of file
454 out_uint32_le(out, 0);
455 out_uint32_le(out, filestat.st_nlink); //Number of links
456 out_uint8(out, 0); //Delete pending
457 out_uint8(out, S_ISDIR(filestat.st_mode) ? 1 : 0); //Directory
458 break;
459
460 case 35: /* FileObjectIdInformation */
461
462 out_uint32_le(out, file_attributes); /* File Attributes */
463 out_uint32_le(out, 0); /* Reparse Tag */
464 break;
465
466 default:
467
468 unimpl("IRP Query (File) Information class: 0x%x\n", info_class);
469 return STATUS_INVALID_PARAMETER;
470 }
471 return STATUS_SUCCESS;
472 }
473
474 NTSTATUS
475 disk_set_information(HANDLE handle, uint32 info_class, STREAM in, STREAM out)
476 {
477 uint32 device_id, length, file_attributes, ft_high, ft_low;
478 char newname[256], fullpath[256];
479 struct fileinfo *pfinfo;
480
481 pfinfo = &(g_fileinfo[handle]);
482
483 switch (info_class)
484 {
485 case 4: /* FileBasicInformation */
486
487 // Probably safe to ignore
488 break;
489
490 case 10: /* FileRenameInformation */
491
492 in_uint8s(in, 4); /* Handle of root dir? */
493 in_uint8s(in, 0x1a); /* unknown */
494 in_uint32_le(in, length);
495
496 if (length && (length / 2) < 256)
497 {
498 rdp_in_unistr(in, newname, length);
499 convert_to_unix_filename(newname);
500 }
501 else
502 {
503 return STATUS_INVALID_PARAMETER;
504 }
505
506 sprintf(fullpath, "%s%s", g_rdpdr_device[pfinfo->device_id].local_path,
507 newname);
508
509 if (rename(pfinfo->path, fullpath) != 0)
510 {
511 perror("rename");
512 return STATUS_ACCESS_DENIED;
513 }
514 break;
515
516 case 13: /* FileDispositionInformation */
517
518 //unimpl("IRP Set File Information class: FileDispositionInformation\n");
519 // in_uint32_le(in, delete_on_close);
520 // disk_close(handle);
521 unlink(pfinfo->path);
522 break;
523
524 case 19: /* FileAllocationInformation */
525
526 unimpl("IRP Set File Information class: FileAllocationInformation\n");
527 break;
528
529 case 20: /* FileEndOfFileInformation */
530
531 unimpl("IRP Set File Information class: FileEndOfFileInformation\n");
532 break;
533
534 default:
535
536 unimpl("IRP Set File Information class: 0x%x\n", info_class);
537 return STATUS_INVALID_PARAMETER;
538 }
539 return STATUS_SUCCESS;
540 }
541
542 NTSTATUS
543 disk_query_volume_information(HANDLE handle, uint32 info_class, STREAM out)
544 {
545 char *volume, *fs_type;
546 struct STATFS_T stat_fs;
547 struct fileinfo *pfinfo;
548
549 pfinfo = &(g_fileinfo[handle]);
550 volume = "RDESKTOP";
551 fs_type = "RDPFS";
552
553 if (STATFS_FN(pfinfo->path, &stat_fs) != 0) /* FIXME: statfs is not portable */
554 {
555 perror("statfs");
556 return STATUS_ACCESS_DENIED;
557 }
558
559 switch (info_class)
560 {
561 case 1: /* FileFsVolumeInformation */
562
563 out_uint32_le(out, 0); /* volume creation time low */
564 out_uint32_le(out, 0); /* volume creation time high */
565 out_uint32_le(out, 0); /* serial */
566 out_uint32_le(out, 2 * strlen(volume)); /* length of string */
567 out_uint8(out, 0); /* support objects? */
568 rdp_out_unistr(out, volume, 2 * strlen(volume) - 2);
569 break;
570
571 case 3: /* FileFsSizeInformation */
572
573 out_uint32_le(out, stat_fs.f_blocks); /* Total allocation units low */
574 out_uint32_le(out, 0); /* Total allocation high units */
575 out_uint32_le(out, stat_fs.f_bfree); /* Available allocation units */
576 out_uint32_le(out, 0); /* Available allowcation units */
577 out_uint32_le(out, stat_fs.f_bsize / 0x200); /* Sectors per allocation unit */
578 out_uint32_le(out, 0x200); /* Bytes per sector */
579 break;
580
581 case 5: /* FileFsAttributeInformation */
582
583 out_uint32_le(out, FS_CASE_SENSITIVE | FS_CASE_IS_PRESERVED); /* fs attributes */
584 out_uint32_le(out, F_NAMELEN(stat_fs)); /* max length of filename */
585 out_uint32_le(out, 2 * strlen(fs_type)); /* length of fs_type */
586 rdp_out_unistr(out, fs_type, 2 * strlen(fs_type) - 2);
587 break;
588
589 case 2: /* FileFsLabelInformation */
590 case 4: /* FileFsDeviceInformation */
591 case 6: /* FileFsControlInformation */
592 case 7: /* FileFsFullSizeInformation */
593 case 8: /* FileFsObjectIdInformation */
594 case 9: /* FileFsMaximumInformation */
595 default:
596
597 unimpl("IRP Query Volume Information class: 0x%x\n", info_class);
598 return STATUS_INVALID_PARAMETER;
599 }
600 return STATUS_SUCCESS;
601 }
602
603 NTSTATUS
604 disk_query_directory(HANDLE handle, uint32 info_class, char *pattern, STREAM out)
605 {
606 uint32 file_attributes, ft_low, ft_high;
607 char *dirname, fullpath[256];
608 DIR *pdir;
609 struct dirent *pdirent;
610 struct stat fstat;
611 struct fileinfo *pfinfo;
612
613 pfinfo = &(g_fileinfo[handle]);
614 pdir = pfinfo->pdir;
615 dirname = pfinfo->path;
616 file_attributes = 0;
617
618 switch (info_class)
619 {
620 case 3: //FIXME: Why 3?
621
622 // If a search pattern is received, remember this pattern, and restart search
623 if (pattern[0] != 0)
624 {
625 strncpy(pfinfo->pattern, 1 + strrchr(pattern, '/'), 64);
626 rewinddir(pdir);
627 }
628
629 // find next dirent matching pattern
630 pdirent = readdir(pdir);
631 while (pdirent && fnmatch(pfinfo->pattern, pdirent->d_name, 0) != 0)
632 {
633 pdirent = readdir(pdir);
634 }
635
636 if (pdirent == NULL)
637 {
638 return STATUS_NO_MORE_FILES;
639 }
640
641 // Get information for directory entry
642 sprintf(fullpath, "%s/%s", dirname, pdirent->d_name);
643 /* JIF
644 printf("Stat: %s\n", fullpath); */
645 if (stat(fullpath, &fstat))
646 {
647 perror("stat");
648 out_uint8(out, 0);
649 return STATUS_ACCESS_DENIED;
650 }
651
652 if (S_ISDIR(fstat.st_mode))
653 file_attributes |= FILE_ATTRIBUTE_DIRECTORY;
654 if (pdirent->d_name[0] == '.')
655 file_attributes |= FILE_ATTRIBUTE_HIDDEN;
656
657 // Return requested information
658 out_uint8s(out, 8); //unknown zero
659 out_uint8s(out, 8); //create_time not available in posix;
660
661 seconds_since_1970_to_filetime(fstat.st_atime, &ft_high, &ft_low);
662 out_uint32_le(out, ft_low); //last_access_time
663 out_uint32_le(out, ft_high);
664
665 seconds_since_1970_to_filetime(fstat.st_mtime, &ft_high, &ft_low);
666 out_uint32_le(out, ft_low); //last_write_time
667 out_uint32_le(out, ft_high);
668
669 out_uint8s(out, 8); //unknown zero
670 out_uint32_le(out, fstat.st_size); //filesize low
671 out_uint32_le(out, 0); //filesize high
672 out_uint32_le(out, fstat.st_size); //filesize low
673 out_uint32_le(out, 0); //filesize high
674 out_uint32_le(out, file_attributes);
675 out_uint8(out, 2 * strlen(pdirent->d_name) + 2); //unicode length
676 out_uint8s(out, 7); //pad?
677 out_uint8(out, 0); //8.3 file length
678 out_uint8s(out, 2 * 12); //8.3 unicode length
679 rdp_out_unistr(out, pdirent->d_name, 2 * strlen(pdirent->d_name));
680 break;
681
682 default:
683
684 unimpl("IRP Query Directory sub: 0x%x\n", info_class);
685 return STATUS_INVALID_PARAMETER;
686 }
687
688 return STATUS_SUCCESS;
689 }
690
691 DEVICE_FNS disk_fns = {
692 disk_create,
693 disk_close,
694 disk_read,
695 disk_write,
696 NULL /* device_control */
697 };

  ViewVC Help
Powered by ViewVC 1.1.26