VirtualBox

source: vbox/trunk/src/VBox/Runtime/common/misc/tar.cpp@ 25607

Last change on this file since 25607 was 25329, checked in by vboxsync, 15 years ago

Runtime: typo

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.3 KB
Line 
1/* $Id: tar.cpp 25329 2009-12-11 13:49:59Z vboxsync $ */
2/** @file
3 * IPRT - Tar archive I/O.
4 */
5
6/*
7 * Copyright (C) 2009 Sun Microsystems, Inc.
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 *
17 * The contents of this file may alternatively be used under the terms
18 * of the Common Development and Distribution License Version 1.0
19 * (CDDL) only, as it comes in the "COPYING.CDDL" file of the
20 * VirtualBox OSE distribution, in which case the provisions of the
21 * CDDL are applicable instead of those of the GPL.
22 *
23 * You may elect to license modified versions of this file under the
24 * terms and conditions of either the GPL or the CDDL or both.
25 *
26 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
27 * Clara, CA 95054 USA or visit http://www.sun.com if you need
28 * additional information or have any questions.
29 */
30
31
32/*******************************************************************************
33* Header Files *
34*******************************************************************************/
35#include "internal/iprt.h"
36#include <iprt/tar.h>
37
38#include <iprt/asm.h>
39#include <iprt/assert.h>
40#include <iprt/err.h>
41#include <iprt/file.h>
42#include <iprt/mem.h>
43#include <iprt/path.h>
44#include <iprt/string.h>
45
46
47/*******************************************************************************
48* Structures and Typedefs *
49*******************************************************************************/
50
51/** @name RTTARRECORD::h::linkflag
52 * @{ */
53#define LF_OLDNORMAL '\0' /**< Normal disk file, Unix compatible */
54#define LF_NORMAL '0' /**< Normal disk file */
55#define LF_LINK '1' /**< Link to previously dumped file */
56#define LF_SYMLINK '2' /**< Symbolic link */
57#define LF_CHR '3' /**< Character special file */
58#define LF_BLK '4' /**< Block special file */
59#define LF_DIR '5' /**< Directory */
60#define LF_FIFO '6' /**< FIFO special file */
61#define LF_CONTIG '7' /**< Contiguous file */
62/** @} */
63
64typedef union RTTARRECORD
65{
66 char d[512];
67 struct h
68 {
69 char name[100];
70 char mode[8];
71 char uid[8];
72 char gid[8];
73 char size[12];
74 char mtime[12];
75 char chksum[8];
76 char linkflag;
77 char linkname[100];
78 char magic[8];
79 char uname[32];
80 char gname[32];
81 char devmajor[8];
82 char devminor[8];
83 } h;
84} RTTARRECORD;
85typedef RTTARRECORD *PRTTARRECORD;
86AssertCompileSize(RTTARRECORD, 512);
87AssertCompileMemberOffset(RTTARRECORD, h.size, 100+8*3);
88
89#if 0 /* not currently used */
90typedef struct RTTARFILELIST
91{
92 char *pszFilename;
93 RTTARFILELIST *pNext;
94} RTTARFILELIST;
95typedef RTTARFILELIST *PRTTARFILELIST;
96#endif
97
98
99/*******************************************************************************
100* Internal Functions *
101*******************************************************************************/
102
103static int rtTarCalcChkSum(PRTTARRECORD pRecord, uint32_t *pChkSum)
104{
105 uint32_t check = 0;
106 uint32_t zero = 0;
107 for (size_t i = 0; i < sizeof(RTTARRECORD); ++i)
108 {
109 /* Calculate the sum of every byte from the header. The checksum field
110 * itself is counted as all blanks. */
111 if ( i < RT_UOFFSETOF(RTTARRECORD, h.chksum)
112 || i >= RT_UOFFSETOF(RTTARRECORD, h.linkflag))
113 check += pRecord->d[i];
114 else
115 check += ' ';
116 /* Additional check if all fields are zero, which indicate EOF. */
117 zero += pRecord->d[i];
118 }
119
120 /* EOF? */
121 if (!zero)
122 return VERR_EOF;
123
124 *pChkSum = check;
125 return VINF_SUCCESS;
126}
127
128static int rtTarCheckHeader(PRTTARRECORD pRecord)
129{
130 uint32_t check;
131 int rc = rtTarCalcChkSum(pRecord, &check);
132 /* EOF? */
133 if (RT_FAILURE(rc))
134 return rc;
135
136 /* Verify the checksum */
137 uint32_t sum;
138 rc = RTStrToUInt32Full(pRecord->h.chksum, 8, &sum);
139 if (RT_SUCCESS(rc) && sum == check)
140 return VINF_SUCCESS;
141 return VERR_TAR_CHKSUM_MISMATCH;
142}
143
144static int rtTarCopyFileFrom(RTFILE hFile, const char *pszTargetName, PRTTARRECORD pRecord)
145{
146 RTFILE hNewFile;
147 /* Open the target file */
148 int rc = RTFileOpen(&hNewFile, pszTargetName, RTFILE_O_CREATE | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE);
149 if (RT_FAILURE(rc))
150 return rc;
151
152/**@todo r=bird: Use a bigger buffer here, see comment in rtTarCopyFileTo. */
153
154 uint64_t cbToCopy = RTStrToUInt64(pRecord->h.size);
155 size_t cbAllWritten = 0;
156 RTTARRECORD record;
157 /* Copy the content from hFile over to pszTargetName. This is done block
158 * wise in 512 byte steps. After this copying is finished hFile will be on
159 * a 512 byte boundary, regardless if the file copied is 512 byte size
160 * aligned. */
161 for (;;)
162 {
163 /* Finished already? */
164 if (cbAllWritten == cbToCopy)
165 break;
166 /* Read one block */
167 rc = RTFileRead(hFile, &record, sizeof(record), NULL);
168 if (RT_FAILURE(rc))
169 break;
170 size_t cbToWrite = sizeof(record);
171 /* Check for the last block which has not to be 512 bytes in size. */
172 if (cbAllWritten + cbToWrite > cbToCopy)
173 cbToWrite = cbToCopy - cbAllWritten;
174 /* Write the block */
175 rc = RTFileWrite(hNewFile, &record, cbToWrite, NULL);
176 if (RT_FAILURE(rc))
177 break;
178 /* Count how many bytes are written already */
179 cbAllWritten += cbToWrite;
180 }
181
182 /* Now set all file attributes */
183 if (RT_SUCCESS(rc))
184 {
185 int32_t mode;
186 rc = RTStrToInt32Full(pRecord->h.mode, 8, &mode);
187 if (RT_SUCCESS(rc))
188 {
189 mode |= RTFS_TYPE_FILE; /* For now we support regular files only */
190 /* Set the mode */
191 rc = RTFileSetMode(hNewFile, mode);
192 }
193 }
194 /* Make sure the called doesn't mix truncated tar files with the official
195 * end indicated by rtTarCalcChkSum. */
196 else if (rc == VERR_EOF)
197 rc = VERR_FILE_IO_ERROR;
198
199 RTFileClose(hNewFile);
200
201 /* Delete the freshly created file in the case of an error */
202 if (RT_FAILURE(rc))
203 RTFileDelete(pszTargetName);
204
205 return rc;
206}
207
208static int rtTarCopyFileTo(RTFILE hFile, const char *pszSrcName)
209{
210 RTFILE hOldFile;
211 /* Open the source file */
212 int rc = RTFileOpen(&hOldFile, pszSrcName, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_WRITE);
213 if (RT_FAILURE(rc))
214 return rc;
215
216 /* Get the size of the source file */
217 uint64_t cbSize;
218 rc = RTFileGetSize(hOldFile, &cbSize);
219 if (RT_FAILURE(rc))
220 {
221 RTFileClose(hOldFile);
222 return rc;
223 }
224 /* Get some info from the source file */
225 RTFSOBJINFO info;
226 RTUID uid = 0;
227 RTGID gid = 0;
228 RTFMODE fmode = 0600; /* Make some save default */
229 int64_t mtime = 0;
230 /* This isn't critical. Use the defaults if it fails. */
231 rc = RTFileQueryInfo(hOldFile, &info, RTFSOBJATTRADD_UNIX);
232 if (RT_SUCCESS(rc))
233 {
234 fmode = info.Attr.fMode & RTFS_UNIX_MASK;
235 uid = info.Attr.u.Unix.uid;
236 gid = info.Attr.u.Unix.gid;
237 mtime = RTTimeSpecGetSeconds(&info.ModificationTime);
238 }
239
240 /* Fill the header record */
241 RTTARRECORD record;
242 RT_ZERO(record);
243 RTStrPrintf(record.h.name, sizeof(record.h.name), "%s", RTPathFilename(pszSrcName));
244 RTStrPrintf(record.h.mode, sizeof(record.h.mode), "%0.7o", fmode);
245 RTStrPrintf(record.h.uid, sizeof(record.h.uid), "%0.7o", uid);
246 RTStrPrintf(record.h.gid, sizeof(record.h.gid), "%0.7o", gid);
247 RTStrPrintf(record.h.size, sizeof(record.h.size), "%0.11o", cbSize);
248 RTStrPrintf(record.h.mtime, sizeof(record.h.mtime), "%0.11o", mtime);
249 RTStrPrintf(record.h.magic, sizeof(record.h.magic), "ustar ");
250 RTStrPrintf(record.h.uname, sizeof(record.h.uname), "someone");
251 RTStrPrintf(record.h.gname, sizeof(record.h.gname), "someone");
252 record.h.linkflag = LF_NORMAL;
253
254 /* Create the checksum out of the new header */
255 uint32_t chksum;
256 rc = rtTarCalcChkSum(&record, &chksum);
257 if (RT_SUCCESS(rc))
258 {
259 RTStrPrintf(record.h.chksum, sizeof(record.h.chksum), "%0.7o", chksum);
260
261 /* Write the header first */
262 rc = RTFileWrite(hFile, &record, sizeof(record), NULL);
263 if (RT_SUCCESS(rc))
264 {
265/** @todo r=bird: using a 64KB buffer here instead of 0.5KB would probably be
266 * a good thing. */
267 uint64_t cbAllWritten = 0;
268 /* Copy the content from pszSrcName over to hFile. This is done block
269 * wise in 512 byte steps. After this copying is finished hFile will be
270 * on a 512 byte boundary, regardless if the file copied is 512 byte
271 * size aligned. */
272 for (;;)
273 {
274 if (cbAllWritten >= cbSize)
275 break;
276 size_t cbToRead = sizeof(record);
277 /* Last record? */
278 if (cbAllWritten + cbToRead > cbSize)
279 {
280 /* Initialize with zeros */
281 RT_ZERO(record);
282 cbToRead = cbSize - cbAllWritten;
283 }
284 /* Read one block */
285 rc = RTFileRead(hOldFile, &record, cbToRead, NULL);
286 if (RT_FAILURE(rc))
287 break;
288 /* Write one block */
289 rc = RTFileWrite(hFile, &record, sizeof(record), NULL);
290 if (RT_FAILURE(rc))
291 break;
292 /* Count how many bytes are written already */
293 cbAllWritten += sizeof(record);
294 }
295
296 /* Make sure the called doesn't mix truncated tar files with the
297 * official end indicated by rtTarCalcChkSum. */
298 if (rc == VERR_EOF)
299 rc = VERR_FILE_IO_ERROR;
300 }
301 }
302
303 RTFileClose(hOldFile);
304 return rc;
305}
306
307static int rtTarSkipData(RTFILE hFile, PRTTARRECORD pRecord)
308{
309 int rc = VINF_SUCCESS;
310 /* Seek over the data parts (512 bytes aligned) */
311 int64_t offSeek = RT_ALIGN(RTStrToInt64(pRecord->h.size), sizeof(RTTARRECORD));
312 if (offSeek > 0)
313 rc = RTFileSeek(hFile, offSeek, RTFILE_SEEK_CURRENT, NULL);
314 return rc;
315}
316
317
318RTR3DECL(int) RTTarQueryFileExists(const char *pszTarFile, const char *pszFile)
319{
320 /* Validate input */
321 AssertPtrReturn(pszTarFile, VERR_INVALID_POINTER);
322 AssertPtrReturn(pszFile, VERR_INVALID_POINTER);
323
324 /* Open the tar file */
325 RTFILE hFile;
326 int rc = RTFileOpen(&hFile, pszTarFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
327 if (RT_FAILURE(rc))
328 return rc;
329
330 bool fFound = false;
331 RTTARRECORD record;
332 for (;;)
333 {
334/** @todo r=bird: the reading, validation and EOF check done here should be
335 * moved to a separate helper function. That would make it easiser to
336 * distinguish genuine-end-of-tar-file and VERR_EOF caused by a
337 * trunacted file. That said, rtTarSkipData won't return VERR_EOF, at
338 * least not on unix, since it's not a sin to seek beyond the end of a
339 * file. */
340 rc = RTFileRead(hFile, &record, sizeof(record), NULL);
341 /* Check for error or EOF. */
342 if (RT_FAILURE(rc))
343 break;
344 /* Check for EOF & data integrity */
345 rc = rtTarCheckHeader(&record);
346 if (RT_FAILURE(rc))
347 break;
348 /* We support normal files only */
349 if ( record.h.linkflag == LF_OLDNORMAL
350 || record.h.linkflag == LF_NORMAL)
351 {
352 if (!RTStrCmp(record.h.name, pszFile))
353 {
354 fFound = true;
355 break;
356 }
357 }
358 rc = rtTarSkipData(hFile, &record);
359 if (RT_FAILURE(rc))
360 break;
361 }
362
363 RTFileClose(hFile);
364
365 if (rc == VERR_EOF)
366 rc = VINF_SUCCESS;
367
368 /* Something found? */
369 if ( RT_SUCCESS(rc)
370 && !fFound)
371 rc = VERR_FILE_NOT_FOUND;
372
373 return rc;
374}
375
376RTR3DECL(int) RTTarList(const char *pszTarFile, char ***ppapszFiles, size_t *pcFiles)
377{
378 /* Validate input */
379 AssertPtrReturn(pszTarFile, VERR_INVALID_POINTER);
380 AssertPtrReturn(ppapszFiles, VERR_INVALID_POINTER);
381 AssertPtrReturn(pcFiles, VERR_INVALID_POINTER);
382
383 /* Open the tar file */
384 RTFILE hFile;
385 int rc = RTFileOpen(&hFile, pszTarFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
386 if (RT_FAILURE(rc))
387 return rc;
388
389 /* Initialize the file name array with one slot */
390 size_t cFilesAlloc = 1;
391 char **papszFiles = (char**)RTMemAlloc(sizeof(char *));
392 if (!papszFiles)
393 {
394 RTFileClose(hFile);
395 return VERR_NO_MEMORY;
396 }
397
398 /* Iterate through the tar file record by record. Skip data records as we
399 * didn't need them. */
400 RTTARRECORD record;
401 size_t cFiles = 0;
402 for (;;)
403 {
404 rc = RTFileRead(hFile, &record, sizeof(record), NULL);
405 /* Check for error or EOF. */
406 if (RT_FAILURE(rc))
407 break;
408 /* Check for EOF & data integrity */
409 rc = rtTarCheckHeader(&record);
410 if (RT_FAILURE(rc))
411 break;
412 /* We support normal files only */
413 if ( record.h.linkflag == LF_OLDNORMAL
414 || record.h.linkflag == LF_NORMAL)
415 {
416 if (cFiles >= cFilesAlloc)
417 {
418 /* Double the array size, make sure the size doesn't wrap. */
419 void *pvNew = NULL;
420 size_t cbNew = cFilesAlloc * sizeof(char *) * 2;
421 if (cbNew / sizeof(char *) / 2 == cFilesAlloc)
422 pvNew = RTMemRealloc(papszFiles, cbNew);
423 if (!pvNew)
424 {
425 rc = VERR_NO_MEMORY;
426 break;
427 }
428 papszFiles = (char **)pvNew;
429 cFilesAlloc *= 2;
430 }
431
432 /* Duplicate the name */
433 papszFiles[cFiles] = RTStrDup(record.h.name);
434 if (!papszFiles[cFiles])
435 {
436 rc = VERR_NO_MEMORY;
437 break;
438 }
439 cFiles++;
440 }
441 rc = rtTarSkipData(hFile, &record);
442 if (RT_FAILURE(rc))
443 break;
444 }
445
446 RTFileClose(hFile);
447
448 if (rc == VERR_EOF)
449 rc = VINF_SUCCESS;
450
451 /* Return the file array on success, dispose of it on failure. */
452 if (RT_SUCCESS(rc))
453 {
454 *pcFiles = cFiles;
455 *ppapszFiles = papszFiles;
456 }
457 else
458 {
459 while (cFiles-- > 0)
460 RTStrFree(papszFiles[cFiles]);
461 RTMemFree(papszFiles);
462 }
463 return rc;
464}
465
466RTR3DECL(int) RTTarExtractFiles(const char *pszTarFile, const char *pszOutputDir, const char * const *papszFiles, size_t cFiles)
467{
468 /* Validate input */
469 AssertPtrReturn(pszTarFile, VERR_INVALID_POINTER);
470 AssertPtrReturn(pszOutputDir, VERR_INVALID_POINTER);
471 AssertPtrReturn(papszFiles, VERR_INVALID_POINTER);
472
473 /* Open the tar file */
474 RTFILE hFile;
475 int rc = RTFileOpen(&hFile, pszTarFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
476 if (RT_FAILURE(rc))
477 return rc;
478
479 /* Iterate through the tar file record by record. */
480 RTTARRECORD record;
481 char **paExtracted = (char **)RTMemTmpAllocZ(sizeof(char *) * cFiles);
482 if (paExtracted)
483 {
484 size_t cExtracted = 0;
485 for (;;)
486 {
487 rc = RTFileRead(hFile, &record, sizeof(record), NULL);
488 /* Check for error or EOF. */
489 if (RT_FAILURE(rc))
490 break;
491 /* Check for EOF & data integrity */
492 rc = rtTarCheckHeader(&record);
493 if (RT_FAILURE(rc))
494 break;
495 /* We support normal files only */
496 if ( record.h.linkflag == LF_OLDNORMAL
497 || record.h.linkflag == LF_NORMAL)
498 {
499 bool fFound = false;
500 for (size_t i = 0; i < cFiles; ++i)
501 {
502 if (!RTStrCmp(record.h.name, papszFiles[i]))
503 {
504 fFound = true;
505 if (cExtracted < cFiles)
506 {
507 char *pszTargetFile;
508 rc = RTStrAPrintf(&pszTargetFile, "%s/%s", pszOutputDir, papszFiles[i]);
509 if (rc > 0)
510 {
511 rc = rtTarCopyFileFrom(hFile, pszTargetFile, &record);
512 if (RT_SUCCESS(rc))
513 paExtracted[cExtracted++] = pszTargetFile;
514 else
515 RTStrFree(pszTargetFile);
516 }
517 else
518 rc = VERR_NO_MEMORY;
519 }
520 else
521 rc = VERR_ALREADY_EXISTS;
522 break;
523 }
524 }
525 if (RT_FAILURE(rc))
526 break;
527 /* If the current record isn't a file in the file list we have to
528 * skip the data */
529 if (!fFound)
530 {
531 rc = rtTarSkipData(hFile, &record);
532 if (RT_FAILURE(rc))
533 break;
534 }
535 }
536 }
537
538 if (rc == VERR_EOF)
539 rc = VINF_SUCCESS;
540
541 /* If we didn't found all files, indicate an error */
542 if (cExtracted != cFiles && RT_SUCCESS(rc))
543 rc = VERR_FILE_NOT_FOUND;
544
545 /* Cleanup the names of the extracted files, deleting them on failure. */
546 while (cExtracted-- > 0)
547 {
548 if (RT_FAILURE(rc))
549 RTFileDelete(paExtracted[cExtracted]);
550 RTStrFree(paExtracted[cExtracted]);
551 }
552 RTMemTmpFree(paExtracted);
553 }
554 else
555 rc = VERR_NO_TMP_MEMORY;
556
557 RTFileClose(hFile);
558 return rc;
559}
560
561RTR3DECL(int) RTTarExtractByIndex(const char *pszTarFile, const char *pszOutputDir, size_t iIndex, char **ppszFileName)
562{
563 /* Validate input */
564 AssertPtrReturn(pszTarFile, VERR_INVALID_POINTER);
565 AssertPtrReturn(pszOutputDir, VERR_INVALID_POINTER);
566
567 /* Open the tar file */
568 RTFILE hFile;
569 int rc = RTFileOpen(&hFile, pszTarFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE);
570 if (RT_FAILURE(rc))
571 return rc;
572
573 /* Iterate through the tar file record by record. */
574 RTTARRECORD record;
575 size_t iFile = 0;
576 bool fFound = false;
577 for (;;)
578 {
579 rc = RTFileRead(hFile, &record, sizeof(record), NULL);
580 /* Check for error or EOF. */
581 if (RT_FAILURE(rc))
582 break;
583 /* Check for EOF & data integrity */
584 rc = rtTarCheckHeader(&record);
585 if (RT_FAILURE(rc))
586 break;
587 /* We support normal files only */
588 if ( record.h.linkflag == LF_OLDNORMAL
589 || record.h.linkflag == LF_NORMAL)
590 {
591 if (iIndex == iFile)
592 {
593 fFound = true;
594 char *pszTargetName;
595 rc = RTStrAPrintf(&pszTargetName, "%s/%s", pszOutputDir, record.h.name);
596 if (rc > 0)
597 {
598 rc = rtTarCopyFileFrom(hFile, pszTargetName, &record);
599 /* On success pass on the filename if requested. */
600 if ( RT_SUCCESS(rc)
601 && ppszFileName)
602 *ppszFileName = pszTargetName;
603 else
604 RTStrFree(pszTargetName);
605 }
606 else
607 rc = VERR_NO_MEMORY;
608 break;
609 }
610 }
611 rc = rtTarSkipData(hFile, &record);
612 if (RT_FAILURE(rc))
613 break;
614 ++iFile;
615 }
616
617 RTFileClose(hFile);
618
619 if (rc == VERR_EOF)
620 rc = VINF_SUCCESS;
621
622 /* If we didn't found the index, indicate an error */
623 if (!fFound && RT_SUCCESS(rc))
624 rc = VERR_FILE_NOT_FOUND;
625
626 return rc;
627}
628
629RTR3DECL(int) RTTarCreate(const char *pszTarFile, const char * const *papszFiles, size_t cFiles)
630{
631 /* Validate input */
632 AssertPtrReturn(pszTarFile, VERR_INVALID_POINTER);
633 AssertPtrReturn(papszFiles, VERR_INVALID_POINTER);
634
635 /* Open the tar file */
636 RTFILE hFile;
637 int rc = RTFileOpen(&hFile, pszTarFile, RTFILE_O_CREATE | RTFILE_O_WRITE | RTFILE_O_DENY_WRITE);
638 if (RT_FAILURE(rc))
639 return rc;
640
641 for (size_t i = 0; i < cFiles; ++i)
642 {
643 rc = rtTarCopyFileTo(hFile, papszFiles[i]);
644 if (RT_FAILURE(rc))
645 break;
646 }
647
648 /* gtar gives a warning, but the documentation says EOF is indicated by a
649 * zero block. Disabled for now. */
650#if 0
651 if (RT_SUCCESS(rc))
652 {
653 /* Append the EOF record which is filled all by zeros */
654 RTTARRECORD record;
655 ASMMemFill32(&record, sizeof(record), 0);
656 rc = RTFileWrite(hFile, &record, sizeof(record), NULL);
657 }
658#endif
659
660 /* Time to close the new tar archive */
661 RTFileClose(hFile);
662
663 /* Delete the freshly created tar archive on failure */
664 if (RT_FAILURE(rc))
665 RTFileDelete(pszTarFile);
666
667 return rc;
668}
669
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette