VirtualBox

Changeset 32973 in vbox


Ignore:
Timestamp:
Oct 7, 2010 11:16:28 AM (14 years ago)
Author:
vboxsync
Message:

Guest Copy/VBoxManage: Implemented support for reading files from an ISO 9660 file (not finished, needs to be moved to some better location later).

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp

    r32888 r32973  
    3737#include <VBox/log.h>
    3838#include <iprt/asm.h>
     39#include <iprt/assert.h>
     40#include <iprt/file.h>
    3941#include <iprt/getopt.h>
     42#include <iprt/list.h>
     43#include <iprt/path.h>
    4044#include <iprt/stream.h>
    4145#include <iprt/string.h>
     
    554558}
    555559
     560#ifdef VBOX_WITH_COPYTOGUEST
     561
     562#define VBOX_ISO9660_MAX_SYSTEM_ID 32
     563#define VBOX_ISO9660_MAX_VOLUME_ID 32
     564#define VBOX_ISO9660_MAX_PUBLISHER_ID   128
     565#define VBOX_ISO9660_MAX_VOLUME_ID   32
     566#define VBOX_ISO9660_MAX_VOLUMESET_ID   128
     567#define VBOX_ISO9660_MAX_PREPARER_ID   128
     568#define VBOX_ISO9660_MAX_APPLICATION_ID   128
     569#define VBOX_ISO9660_STANDARD_ID "CD001"
     570#define VBOX_ISO9660_SECTOR_SIZE 2048
     571
     572#pragma pack(1)
     573typedef struct VBoxISO9660DateShort
     574{
     575    uint8_t year;
     576    uint8_t month;
     577    uint8_t day;
     578    uint8_t hour;
     579    uint8_t minute;
     580    uint8_t second;
     581    int8_t      gmt_offset;
     582} VBoxISO9660DateShort;
     583
     584typedef struct VBoxISO9660DateLong
     585{
     586    char year[4];
     587    char month[2];
     588    char day[2];
     589    char hour[2];
     590    char minute[2];
     591    char second[2];
     592    char hseconds[2];
     593    int8_t gmt_offset;
     594} VBoxISO9660DateLong;
     595
     596typedef struct VBoxISO9660DirRecord
     597{
     598        uint8_t record_length;
     599        uint8_t extented_attr_length;
     600        uint32_t extent_location;
     601    uint32_t extent_location_big;
     602        uint32_t extent_data_length; /* Number of bytes (file) / len (directory). */
     603    uint32_t extent_data_length_big;
     604    VBoxISO9660DateShort date;
     605        uint8_t flags;
     606        uint8_t interleave_unit_size;
     607    uint8_t interleave_gap_size;
     608        uint16_t volume_sequence_number;
     609    uint16_t volume_sequence_number_big;
     610        uint8_t name_len;
     611    /* Starting here there will be the actual directory entry name
     612     * and a padding of 1 byte if name_len is odd. */
     613} VBoxISO9660DirRecord;
     614
     615typedef struct VBoxISO9660PriVolDesc
     616{
     617        uint8_t type;
     618        char name_id[6];
     619        uint8_t version;
     620        char system_id[VBOX_ISO9660_MAX_SYSTEM_ID];
     621        char volume_id[VBOX_ISO9660_MAX_VOLUME_ID];
     622        uint8_t unused2[8];
     623        uint32_t volume_space_size; /* Number of sectors, Little Endian. */
     624        uint32_t volume_space_size_big; /* Number of sectors Big Endian. */
     625    uint8_t unused3[32];
     626        uint16_t volume_set_size;
     627    uint16_t volume_set_size_big;
     628    uint16_t volume_sequence_number;
     629    uint16_t volume_sequence_number_big;
     630    uint16_t logical_block_size; /* 2048. */
     631    uint16_t logical_block_size_big;
     632        uint32_t path_table_size; /* Size in bytes. */
     633        uint32_t path_table_size_big; /* Size in bytes. */
     634    uint32_t path_table_start_first;
     635    uint32_t path_table_start_second;
     636    uint32_t path_table_start_first_big;
     637    uint32_t path_table_start_second_big;
     638    VBoxISO9660DirRecord root_directory_record;
     639    uint8_t directory_padding;
     640    char volume_set_id[VBOX_ISO9660_MAX_VOLUMESET_ID];
     641    char publisher_id[VBOX_ISO9660_MAX_PUBLISHER_ID];
     642    char preparer_id[VBOX_ISO9660_MAX_PREPARER_ID];
     643    char application_id[VBOX_ISO9660_MAX_APPLICATION_ID];
     644    char copyright_file_id[37];
     645    char abstract_file_id[37];
     646    char bibliographic_file_id[37];
     647    VBoxISO9660DateLong creation_date;
     648    VBoxISO9660DateLong modification_date;
     649    VBoxISO9660DateLong expiration_date;
     650    VBoxISO9660DateLong effective_date;
     651    uint8_t file_structure_version;
     652    uint8_t unused4[1];
     653    char application_data[512];
     654    uint8_t unused5[653];
     655} VBoxISO9660PriVolDesc;
     656
     657typedef struct VBoxISO9660PathTableHeader
     658{
     659    uint8_t length;
     660    uint8_t extended_attr_sectors;
     661    /** Sector of starting directory table. */
     662    uint32_t sector_dir_table;
     663    /** Index of parent directory (1 for the root). */
     664    uint16_t parent_index;
     665    /* Starting here there will be the name of the directory,
     666     * specified by length above. */
     667} VBoxISO9660PathTableHeader;
     668
     669typedef struct VBoxISO9660PathTableEntry
     670{
     671    char       *path;
     672    char       *path_full;
     673    VBoxISO9660PathTableHeader header;
     674    RTLISTNODE  Node;
     675} VBoxISO9660PathTableEntry;
     676
     677typedef struct VBoxISO9660File
     678{
     679    RTFILE file;
     680    RTLISTNODE listPaths;
     681    VBoxISO9660PriVolDesc priVolDesc;
     682} VBoxISO9660File;
     683#pragma pack()
     684
     685void rtISO9660DestroyPathCache(VBoxISO9660File *pFile)
     686{
     687    VBoxISO9660PathTableEntry *pNode = RTListNodeGetFirst(&pFile->listPaths, VBoxISO9660PathTableEntry, Node);
     688    while (pNode)
     689    {
     690        VBoxISO9660PathTableEntry *pNext = RTListNodeGetNext(&pNode->Node, VBoxISO9660PathTableEntry, Node);
     691        bool fLast = RTListNodeIsLast(&pFile->listPaths, &pNode->Node);
     692
     693        if (pNode->path)
     694            RTStrFree(pNode->path);
     695        if (pNode->path_full)
     696            RTStrFree(pNode->path_full);
     697        RTListNodeRemove(&pNode->Node);
     698        RTMemFree(pNode);
     699
     700        if (fLast)
     701            break;
     702
     703        pNode = pNext;
     704    }
     705}
     706
     707void RTISO9660Close(VBoxISO9660File *pFile)
     708{
     709    if (pFile)
     710    {
     711        rtISO9660DestroyPathCache(pFile);
     712        RTFileClose(pFile->file);
     713    }
     714}
     715
     716int rtISO9660AddToPathCache(PRTLISTNODE pList, const char *pszPath,
     717                            VBoxISO9660PathTableHeader *pHeader)
     718{
     719    AssertPtrReturn(pList, VERR_INVALID_PARAMETER);
     720    AssertPtrReturn(pszPath, VERR_INVALID_PARAMETER);
     721    AssertPtrReturn(pHeader, VERR_INVALID_PARAMETER);
     722
     723    VBoxISO9660PathTableEntry *pNode = (VBoxISO9660PathTableEntry*)RTMemAlloc(sizeof(VBoxISO9660PathTableEntry));
     724    if (pNode == NULL)
     725        return VERR_NO_MEMORY;
     726
     727    pNode->path = NULL;
     728    if (RT_SUCCESS(RTStrAAppend(&pNode->path, pszPath)))
     729    {
     730        memcpy((VBoxISO9660PathTableHeader*)&pNode->header,
     731               (VBoxISO9660PathTableHeader*)pHeader, sizeof(pNode->header));
     732
     733        pNode->path_full = NULL;
     734        pNode->Node.pPrev = NULL;
     735        pNode->Node.pNext = NULL;
     736        RTListAppend(pList, &pNode->Node);
     737        return VINF_SUCCESS;
     738    }
     739    return VERR_NO_MEMORY;
     740}
     741
     742int rtISO9660GetParentPathSub(PRTLISTNODE pList, VBoxISO9660PathTableEntry *pNode,
     743                              char *pszPathNode, char **ppszPath)
     744{
     745    int rc = VINF_SUCCESS;
     746    if (pNode->header.parent_index > 1)
     747    {
     748        uint16_t idx = 1;
     749        VBoxISO9660PathTableEntry *pNodeParent = RTListNodeGetFirst(pList, VBoxISO9660PathTableEntry, Node);
     750        while (idx++ < pNode->header.parent_index)
     751            pNodeParent =  RTListNodeGetNext(&pNodeParent->Node, VBoxISO9660PathTableEntry, Node);
     752        char *pszPath;
     753        if (RTStrAPrintf(&pszPath, "%s/%s", pNodeParent->path, pszPathNode))
     754        {
     755            rc = rtISO9660GetParentPathSub(pList, pNodeParent, pszPath, ppszPath);
     756            RTStrFree(pszPath);
     757        }
     758        else
     759            rc = VERR_NO_MEMORY;
     760    }
     761    else
     762    {
     763        char *pszPath = RTStrDup(pszPathNode);
     764        *ppszPath = pszPath;
     765    }
     766    return rc;
     767}
     768
     769/* Is a *flat* structure! */
     770int rtISO9660UpdatePathCache(VBoxISO9660File *pFile)
     771{
     772    AssertPtrReturn(pFile, VERR_INVALID_PARAMETER);
     773    rtISO9660DestroyPathCache(pFile);
     774
     775    RTListInit(&pFile->listPaths);
     776
     777    /* Seek to path tables. */
     778    int rc = VINF_SUCCESS;
     779    Assert(pFile->priVolDesc.path_table_start_first > 16);
     780    uint64_t uTableStart = (pFile->priVolDesc.path_table_start_first * VBOX_ISO9660_SECTOR_SIZE);
     781    Assert(uTableStart % VBOX_ISO9660_SECTOR_SIZE == 0); /* Make sure it's aligned. */
     782    if (RTFileTell(pFile->file) != uTableStart)
     783        rc = RTFileSeek(pFile->file, uTableStart, RTFILE_SEEK_BEGIN, &uTableStart);
     784
     785    /*
     786     * Since this is a sequential format, for performance it's best to read the
     787     * complete path table (every entry can have its own level (directory depth) first
     788     * and the actual directories of the path table afterwards.
     789     */
     790
     791    /* Read in the path table ... */
     792    uint32_t cbLeft = pFile->priVolDesc.path_table_size;
     793    VBoxISO9660PathTableHeader header;
     794    while ((cbLeft > 0) && RT_SUCCESS(rc))
     795    {
     796        size_t cbRead;
     797        rc = RTFileRead(pFile->file, (VBoxISO9660PathTableHeader*)&header, sizeof(VBoxISO9660PathTableHeader), &cbRead);
     798        if (RT_FAILURE(rc))
     799            break;
     800        cbLeft -= cbRead;
     801        if (header.length)
     802        {
     803            Assert(cbLeft >= header.length);
     804            Assert(header.length <= 31);
     805            /* Allocate and read in the actual path name. */
     806            char *pszName = RTStrAlloc(header.length + 1);
     807            rc = RTFileRead(pFile->file, (char*)pszName, header.length, &cbRead);
     808            if (RT_SUCCESS(rc))
     809            {
     810                cbLeft -= cbRead;
     811                pszName[cbRead] = '\0'; /* Terminate string. */
     812                /* Add entry to cache ... */
     813                rc = rtISO9660AddToPathCache(&pFile->listPaths, pszName, &header);
     814            }
     815            RTStrFree(pszName);
     816            /* Read padding if required ... */
     817            if ((header.length % 2) != 0) /* If we have an odd length, read/skip the padding byte. */
     818            {
     819                rc = RTFileSeek(pFile->file, 1, RTFILE_SEEK_CURRENT, NULL);
     820                cbLeft--;
     821            }
     822        }
     823    }
     824
     825    /* Transform path names into full paths. This is a bit ugly right now. */
     826    VBoxISO9660PathTableEntry *pNode = RTListNodeGetLast(&pFile->listPaths, VBoxISO9660PathTableEntry, Node);
     827    while (   pNode
     828           && !RTListNodeIsFirst(&pFile->listPaths, &pNode->Node)
     829           && RT_SUCCESS(rc))
     830    {
     831        rc = rtISO9660GetParentPathSub(&pFile->listPaths, pNode,
     832                                       pNode->path, &pNode->path_full);
     833        if (RT_SUCCESS(rc))
     834            pNode = RTListNodeGetPrev(&pNode->Node, VBoxISO9660PathTableEntry, Node);
     835    }
     836
     837    return rc;
     838}
     839
     840int RTISO9660Open(const char *pszFileName, VBoxISO9660File *pFile)
     841{
     842    AssertPtrReturn(pszFileName, VERR_INVALID_PARAMETER);
     843    AssertPtrReturn(pFile, VERR_INVALID_PARAMETER);
     844
     845    RTListInit(&pFile->listPaths);
     846#if 1
     847    Assert(sizeof(VBoxISO9660DateShort) == 7);
     848    Assert(sizeof(VBoxISO9660DateLong) == 17);
     849    int l = sizeof(VBoxISO9660DirRecord);
     850    RTPrintf("VBoxISO9660DirRecord=%ld\n", l);
     851    Assert(l == 33);
     852    /* Each volume descriptor exactly occupies one sector. */
     853    l = sizeof(VBoxISO9660PriVolDesc);
     854    RTPrintf("VBoxISO9660PriVolDesc=%ld\n", l);
     855    Assert(l == VBOX_ISO9660_SECTOR_SIZE);
     856#endif
     857    int rc = RTFileOpen(&pFile->file, pszFileName, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_WRITE);
     858    if (RT_SUCCESS(rc))
     859    {
     860        uint64_t cbSize;
     861        rc = RTFileGetSize(pFile->file, &cbSize);
     862        if (   RT_SUCCESS(rc)
     863            && cbSize > 16 * VBOX_ISO9660_SECTOR_SIZE)
     864        {
     865            uint64_t cbOffset = 16 * VBOX_ISO9660_SECTOR_SIZE; /* Start reading at 32k. */
     866            size_t cbRead;
     867            VBoxISO9660PriVolDesc volDesc;
     868            bool fFoundPrimary = false;
     869            bool fIsValid = false;
     870            while (cbOffset < _1M)
     871            {
     872                /* Get primary descriptor. */
     873                rc = RTFileRead(pFile->file, (VBoxISO9660PriVolDesc*)&volDesc, sizeof(VBoxISO9660PriVolDesc), &cbRead);
     874                if (RT_FAILURE(rc) || cbRead < sizeof(VBoxISO9660PriVolDesc))
     875                    break;
     876                if (   RTStrStr((char*)volDesc.name_id, VBOX_ISO9660_STANDARD_ID)
     877                    && volDesc.type    == 0x1 /* Primary Volume Descriptor */)
     878                {
     879                    memcpy((VBoxISO9660PriVolDesc*)&pFile->priVolDesc,
     880                           (VBoxISO9660PriVolDesc*)&volDesc, sizeof(VBoxISO9660PriVolDesc));
     881                    fFoundPrimary = true;
     882                }
     883                else if(volDesc.type == 0xff /* Termination Volume Descriptor */)
     884                {
     885                    if (fFoundPrimary)
     886                        fIsValid = true;
     887                    break;
     888                }
     889                cbOffset += sizeof(VBoxISO9660PriVolDesc);
     890            }
     891
     892            if (fIsValid)
     893                rc = rtISO9660UpdatePathCache(pFile);
     894            else
     895                rc = VERR_INVALID_PARAMETER;
     896        }
     897        if (RT_FAILURE(rc))
     898            RTISO9660Close(pFile);
     899    }
     900    return rc;
     901}
     902
     903/* Parses the extent content given at a specified sector. */
     904int rtISO9660FindEntry(VBoxISO9660File *pFile, const char *pszFileName,
     905                       uint32_t uExtentSector, uint32_t cbExtent /* Bytes */,
     906                       VBoxISO9660DirRecord **ppRec)
     907{
     908    AssertPtrReturn(pFile, VERR_INVALID_PARAMETER);
     909    Assert(uExtentSector > 16);
     910
     911    int rc = RTFileSeek(pFile->file, uExtentSector * VBOX_ISO9660_SECTOR_SIZE,
     912                        RTFILE_SEEK_BEGIN, NULL);
     913    if (RT_SUCCESS(rc))
     914    {
     915        rc = VERR_FILE_NOT_FOUND;
     916
     917        uint8_t uBuffer[VBOX_ISO9660_SECTOR_SIZE];
     918        uint32_t cbLeft = cbExtent;
     919        while (!RT_SUCCESS(rc) && cbLeft > 0)
     920        {
     921            size_t cbRead;
     922            int rc2 = RTFileRead(pFile->file, (void*)&uBuffer, sizeof(uBuffer), &cbRead);
     923            Assert(RT_SUCCESS(rc2) && cbRead == VBOX_ISO9660_SECTOR_SIZE);
     924            cbLeft -= cbRead;
     925
     926            uint32_t idx = 0;
     927            while (idx < cbRead)
     928            {
     929                VBoxISO9660DirRecord *pCurRecord = (VBoxISO9660DirRecord*)&uBuffer[idx];
     930                if (pCurRecord->record_length == 0)
     931                    break;
     932
     933                Assert(pCurRecord->name_len > 0);
     934                char *pszName = RTStrAlloc(pCurRecord->name_len + 1);
     935                AssertPtr(pszName);
     936                Assert(idx + sizeof(VBoxISO9660DirRecord) < cbRead);
     937                memcpy(pszName, &uBuffer[idx + sizeof(VBoxISO9660DirRecord)], pCurRecord->name_len);
     938
     939                if (   pCurRecord->name_len == 1
     940                    && pszName[0] == 0x0)
     941                {
     942                    /* This is a "." directory (self). */
     943                }
     944                else if (   pCurRecord->name_len == 1
     945                         && pszName[0] == 0x1)
     946                {
     947                    /* This is a ".." directory (parent). */
     948                }
     949                else /* Regular directory or file */
     950                {
     951                    if (pCurRecord->flags & RT_BIT(1)) /* Directory */
     952                    {
     953                        /* We don't recursively go into directories
     954                         * because we already have the cached path table. */
     955                        pszName[pCurRecord->name_len] = 0;
     956                        /*rc = rtISO9660ParseDir(pFile, pszFileName,
     957                                                 pDirHdr->extent_location, pDirHdr->extent_data_length);*/
     958                    }
     959                    else /* File */
     960                    {
     961                        /* Get last occurence of ";" and cut it off. */
     962                        char *pTerm = strrchr(pszName, ';');
     963                        if (pTerm)
     964                            pszName[pTerm - pszName] = 0;
     965
     966                        /* Don't use case sensitive comparison here, in IS0 9660 all
     967                         * file / directory names are UPPERCASE. */
     968                        if (!RTStrICmp(pszName, pszFileName))
     969                        {
     970                            VBoxISO9660DirRecord *pRec = (VBoxISO9660DirRecord*)RTMemAlloc(sizeof(VBoxISO9660DirRecord));
     971                            if (pRec)
     972                            {
     973                                memcpy(pRec, pCurRecord, sizeof(VBoxISO9660DirRecord));
     974                                *ppRec = pRec;
     975                                rc = VINF_SUCCESS;
     976                                break;
     977                            }
     978                            else
     979                                rc = VERR_NO_MEMORY;
     980                        }
     981                    }
     982                }
     983                idx += pCurRecord->record_length;
     984            }
     985        }
     986    }
     987    return rc;
     988}
     989
     990int rtISO9660ResolvePath(VBoxISO9660File *pFile, const char *pszPath, uint32_t *puSector)
     991{
     992    AssertPtrReturn(pFile, VERR_INVALID_PARAMETER);
     993    AssertPtrReturn(pszPath, VERR_INVALID_PARAMETER);
     994    AssertPtrReturn(puSector, VERR_INVALID_PARAMETER);
     995
     996    int rc = VERR_FILE_NOT_FOUND;
     997    char *pszTemp = RTStrDup(pszPath);
     998    if (pszTemp)
     999    {
     1000        RTPathStripFilename(pszTemp);
     1001
     1002        bool bFound = false;
     1003        VBoxISO9660PathTableEntry *pNode;
     1004        if (!RTStrCmp(pszTemp, ".")) /* Root directory? Use first node! */
     1005        {
     1006            pNode = RTListNodeGetFirst(&pFile->listPaths, VBoxISO9660PathTableEntry, Node);
     1007            bFound = true;
     1008        }
     1009        else
     1010        {
     1011            RTListForEach(&pFile->listPaths, pNode, VBoxISO9660PathTableEntry, Node)
     1012            {
     1013                if (   pNode->path_full != NULL /* Root does not have a path! */
     1014                    && !RTStrICmp(pNode->path_full, pszTemp))
     1015                {
     1016                    bFound = true;
     1017                    break;
     1018                }
     1019            }
     1020        }
     1021        if (bFound)
     1022        {
     1023            *puSector = pNode->header.sector_dir_table;
     1024            rc = VINF_SUCCESS;
     1025        }
     1026        RTStrFree(pszTemp);
     1027    }
     1028    else
     1029        rc = VERR_NO_MEMORY;
     1030    return rc;
     1031}
     1032
     1033int rtISO9660ReadFileInternal(VBoxISO9660File *pFile, const char *pszPath,
     1034                              uint32_t cbOffset, size_t cbToRead, size_t *pcbRead)
     1035{
     1036    AssertPtrReturn(pFile, VERR_INVALID_PARAMETER);
     1037    AssertPtrReturn(pszPath, VERR_INVALID_PARAMETER);
     1038
     1039    uint32_t uSector;
     1040    int rc = rtISO9660ResolvePath(pFile, pszPath, &uSector);
     1041    if (RT_SUCCESS(rc))
     1042    {
     1043        /* Seek to directory table. */
     1044        rc = RTFileSeek(pFile->file, uSector * VBOX_ISO9660_SECTOR_SIZE,
     1045                        RTFILE_SEEK_BEGIN, NULL);
     1046        if (RT_SUCCESS(rc))
     1047        {
     1048            size_t cbRead;
     1049            VBoxISO9660DirRecord dir_table;
     1050            rc = RTFileRead(pFile->file, (VBoxISO9660DirRecord*)&dir_table, sizeof(VBoxISO9660DirRecord), &cbRead);
     1051            if (RT_SUCCESS(rc))
     1052            {
     1053                Assert(cbRead == sizeof(VBoxISO9660DirRecord));
     1054                VBoxISO9660DirRecord *pRecord = NULL;
     1055                rc = rtISO9660FindEntry(pFile,
     1056                                        RTPathFilename(pszPath),
     1057                                        dir_table.extent_location,
     1058                                        dir_table.extent_data_length,
     1059                                        &pRecord);
     1060                if (   RT_SUCCESS(rc)
     1061                    && pRecord)
     1062                {
     1063                    RTMemFree(pRecord);
     1064                }
     1065            }
     1066            else
     1067                rc = VERR_INVALID_PARAMETER;
     1068        }
     1069    }
     1070    return rc;
     1071}
     1072
     1073int RTISO9660ReadFile(VBoxISO9660File *pFile, const char *pszFileName,
     1074                      uint32_t cbOffset, size_t cbToRead, size_t *pcbRead)
     1075{
     1076    return rtISO9660ReadFileInternal(pFile, pszFileName, cbOffset, cbToRead, pcbRead);
     1077}
     1078#endif
     1079
    5561080static int handleCtrlCopyTo(HandlerArg *a)
    5571081{
     1082    VBoxISO9660File file;
     1083    int vrc = RTISO9660Open("c:\\Downloads\\VBoxGuestAdditions_3.2.8.iso", &file);
     1084    if (RT_SUCCESS(vrc))
     1085    {
     1086        uint32_t uOffset = 0;
     1087        size_t cbRead;
     1088        while (RT_SUCCESS(vrc))
     1089        {
     1090            vrc = RTISO9660ReadFile(&file, "32BIT/OS2/readme.txt",
     1091                                    uOffset, _4K, &cbRead);
     1092            uOffset += cbRead;
     1093        }
     1094        RTISO9660Close(&file);
     1095    }
     1096    return vrc;
     1097
    5581098    /*
    5591099     * Check the syntax.  We can deduce the correct syntax from the number of
Note: See TracChangeset for help on using the changeset viewer.

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