2021-10-11 04:48:44 +02:00
|
|
|
/****************************************************************************
|
|
|
|
* fs/partition/fs_gpt.c
|
|
|
|
*
|
|
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
|
|
* this work for additional information regarding copyright ownership. The
|
|
|
|
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
|
|
|
* "License"); you may not use this file except in compliance with the
|
|
|
|
* License. You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
* License for the specific language governing permissions and limitations
|
|
|
|
* under the License.
|
|
|
|
*
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Included Files
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
#include <ctype.h>
|
|
|
|
#include <debug.h>
|
|
|
|
#include <endian.h>
|
2022-07-19 13:47:38 +02:00
|
|
|
#include <inttypes.h>
|
2023-02-02 17:05:40 +01:00
|
|
|
#include <sys/param.h>
|
2021-10-11 04:48:44 +02:00
|
|
|
|
2022-08-12 08:13:41 +02:00
|
|
|
#include <nuttx/crc32.h>
|
2021-10-11 04:48:44 +02:00
|
|
|
#include <nuttx/kmalloc.h>
|
|
|
|
|
|
|
|
#include "partition.h"
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Pre-processor Definitions
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
#define GPT_BLOCK_SIZE 512
|
|
|
|
#define GPT_HEADER_SIGNATURE 0x5452415020494645ull
|
|
|
|
#define GPT_PARTNAME_MAX_SIZE (72 / sizeof(uint16_t))
|
2023-05-19 00:16:35 +02:00
|
|
|
#define GPT_LBA_TO_BLOCK(lba, blk) ((le64toh(lba) * 512 + (blk) - 1) / (blk))
|
2021-10-11 04:48:44 +02:00
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Private Types
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
struct gpt_guid_s
|
|
|
|
{
|
|
|
|
uint8_t b[16];
|
|
|
|
};
|
|
|
|
|
|
|
|
/* For limited backward compatibility, the space of the legacy MBR is still
|
|
|
|
* reserved in the GPT specification, but it is now used in a way that
|
|
|
|
* prevents MBR-based disk utilities from misrecognizing and possibly
|
|
|
|
* overwriting GPT disks. This is referred to as a protective MBR.
|
|
|
|
*/
|
|
|
|
|
|
|
|
begin_packed_struct struct legacy_partition_s
|
|
|
|
{
|
|
|
|
uint8_t boot_ind; /* 0x80 - active */
|
|
|
|
uint8_t head; /* Starting head */
|
|
|
|
uint8_t sector; /* Starting sector */
|
|
|
|
uint8_t cyl; /* Starting cylinder */
|
|
|
|
uint8_t sys_ind; /* What partition type */
|
|
|
|
uint8_t end_head; /* End head */
|
|
|
|
uint8_t end_sector; /* End sector */
|
|
|
|
uint8_t end_cyl; /* End cylinder */
|
|
|
|
uint32_t start_sect; /* Starting sector counting from 0 */
|
|
|
|
uint32_t nr_sects; /* Nr of sectors in partition */
|
|
|
|
} end_packed_struct;
|
|
|
|
|
|
|
|
/* The partition table header defines the usable blocks on the disk.
|
|
|
|
* It also defines the number and size of the partition entries that
|
|
|
|
* make up the partition table (offsets 80 and 84 in the table).
|
|
|
|
*/
|
|
|
|
|
|
|
|
begin_packed_struct struct gpt_header_s
|
|
|
|
{
|
|
|
|
uint64_t signature; /* EFI PART */
|
|
|
|
uint32_t revision; /* Revision info */
|
|
|
|
uint32_t header_size; /* Header size in little endian */
|
|
|
|
uint32_t header_crc32; /* CRC32 of header (offset +0 up to header size) */
|
|
|
|
uint32_t reserved1; /* Must be zero */
|
|
|
|
uint64_t my_lba; /* Current LBA (location of this header copy) */
|
|
|
|
uint64_t alternate_lba; /* Backup LBA (location of the other header copy) */
|
|
|
|
uint64_t first_usable_lba; /* First usable LBA for partitions primary partition table last LBA + 1 */
|
2023-09-18 10:31:07 +02:00
|
|
|
uint64_t last_usable_lba; /* Last usable LBA secondary partition table first LBA - 1 */
|
2021-10-11 04:48:44 +02:00
|
|
|
struct gpt_guid_s disk_guid; /* Disk GUID in mixed endian */
|
|
|
|
uint64_t partition_entry_lba; /* Starting LBA of array of partition entries (always 2 in primary copy) */
|
|
|
|
uint32_t num_partition_entries; /* Number of partition entries in array */
|
|
|
|
uint32_t sizeof_partition_entry; /* Size of a single partition entry */
|
|
|
|
uint32_t partition_entry_array_crc32; /* CRC32 of partition entries array in little endian */
|
|
|
|
|
|
|
|
/* The rest of the logical block is reserved by UEFI and must be zero.
|
|
|
|
* EFI standard handles this by:
|
|
|
|
*
|
|
|
|
* uint8_t reserved2[ BlockSize - 92 ];
|
|
|
|
*/
|
|
|
|
} end_packed_struct;
|
|
|
|
|
|
|
|
/* After the header, the Partition Entry Array describes partitions,
|
|
|
|
* using a minimum size of 128 bytes for each entry block.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* The 64-bit partition table attributes are shared between 48-bit
|
|
|
|
* common attributes for all partition types, and 16-bit
|
|
|
|
* type-specific attributes
|
|
|
|
*/
|
|
|
|
|
|
|
|
begin_packed_struct struct gpt_entry_attributes_s
|
|
|
|
{
|
|
|
|
uint64_t required_to_function:1;
|
|
|
|
uint64_t reserved:47;
|
|
|
|
uint64_t type_guid_specific:16;
|
|
|
|
} end_packed_struct;
|
|
|
|
|
|
|
|
begin_packed_struct struct gpt_entry_s
|
|
|
|
{
|
|
|
|
struct gpt_guid_s partition_type_guid; /* Partition type GUID */
|
|
|
|
struct gpt_guid_s unique_partition_guid; /* Unique partition GUID */
|
|
|
|
uint64_t starting_lba; /* First LBA */
|
|
|
|
uint64_t ending_lba; /* Last LBA */
|
|
|
|
struct gpt_entry_attributes_s attributes; /* Attribute flags */
|
|
|
|
uint16_t partition_name[GPT_PARTNAME_MAX_SIZE]; /* Partition name */
|
|
|
|
} end_packed_struct;
|
|
|
|
|
|
|
|
begin_packed_struct struct gpt_ptable_s
|
|
|
|
{
|
|
|
|
uint8_t mbr[512];
|
|
|
|
union
|
|
|
|
{
|
|
|
|
struct gpt_header_s gpt_header;
|
|
|
|
uint8_t gpt[512];
|
|
|
|
} u;
|
|
|
|
} end_packed_struct;
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Private Data
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
static const struct gpt_guid_s g_null_guid;
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Private Functions
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Name: gpt_last_lba
|
|
|
|
*
|
|
|
|
* Description:
|
|
|
|
* Return number of last logical block of device, 0 on error.
|
|
|
|
*
|
|
|
|
* Input Parameters:
|
|
|
|
* state - The partition table state
|
|
|
|
*
|
|
|
|
* Returned Value:
|
|
|
|
* Returns last LBA value on success, 0 on error.
|
|
|
|
* This is stored (by sd and ide-geometry) in
|
|
|
|
* the part[0] entry for this disk, and is the number of
|
|
|
|
* physical sectors available on the disk.
|
|
|
|
*
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
static inline blkcnt_t gpt_last_lba(FAR struct partition_state_s *state)
|
|
|
|
{
|
2022-01-19 12:44:33 +01:00
|
|
|
return (((uint64_t)state->nblocks) * state->blocksize + GPT_BLOCK_SIZE - 1)
|
|
|
|
/ GPT_BLOCK_SIZE - 1;
|
2021-10-11 04:48:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Name: gpt_alloc_verify_entries()
|
|
|
|
*
|
|
|
|
* Description:
|
|
|
|
* reads and verifies partition entries from disk
|
|
|
|
*
|
|
|
|
* Input Parameters:
|
|
|
|
* state - the handle of partition state
|
|
|
|
* gpt - a GPT header ptr.
|
|
|
|
*
|
|
|
|
* Returned Value:
|
|
|
|
* Returns ptes on success, NULL on error.
|
|
|
|
* Allocates space for PTEs based on information found in @gpt.
|
|
|
|
* Notes: remember to free pte when you're done!
|
|
|
|
*
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
static FAR struct gpt_entry_s *
|
|
|
|
gpt_alloc_verify_entries(FAR struct partition_state_s *state,
|
|
|
|
FAR struct gpt_header_s *gpt)
|
|
|
|
{
|
|
|
|
FAR struct gpt_entry_s *pte;
|
|
|
|
unsigned long from;
|
|
|
|
unsigned long size;
|
|
|
|
unsigned long blk;
|
|
|
|
uint32_t crc;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
size = le32toh(gpt->num_partition_entries) *
|
|
|
|
le32toh(gpt->sizeof_partition_entry);
|
|
|
|
if (!size)
|
|
|
|
{
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2023-05-19 00:16:35 +02:00
|
|
|
blk = (size + (state->blocksize - 1)) / state->blocksize;
|
2021-10-11 04:48:44 +02:00
|
|
|
pte = kmm_zalloc(blk * state->blocksize);
|
|
|
|
if (!pte)
|
|
|
|
{
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
from = GPT_LBA_TO_BLOCK(gpt->partition_entry_lba,
|
|
|
|
state->blocksize);
|
|
|
|
ret = read_partition_block(state, pte, from, blk);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
kmm_free(pte);
|
|
|
|
ferr("Read ptr from block failed:%d.\n", ret);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Check the GUID Partition Table Entry Array CRC */
|
|
|
|
|
|
|
|
crc = crc32part((FAR const uint8_t *)pte, size, ~0l) ^ ~0l;
|
|
|
|
if (crc != le32toh(gpt->partition_entry_array_crc32))
|
|
|
|
{
|
|
|
|
ferr("GUID Partitition Entry Array CRC check failed.\n");
|
|
|
|
kmm_free(pte);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
return pte;
|
|
|
|
}
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Name: gpt_header_is_valid
|
|
|
|
*
|
|
|
|
* Description:
|
|
|
|
* tests one GPT header for validity
|
|
|
|
*
|
|
|
|
* Input Parameters:
|
|
|
|
* state - The partition table state
|
|
|
|
* gpt - is a GPT header ptr.
|
|
|
|
* lba - is the logical block address of the GPT header to test
|
|
|
|
*
|
|
|
|
* Returned Value:
|
|
|
|
* Returns 0 if valid, a negative errno returned on error.
|
|
|
|
*
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
static int gpt_header_is_valid(FAR struct partition_state_s *state,
|
|
|
|
FAR struct gpt_header_s *gpt, blkcnt_t lba)
|
|
|
|
{
|
|
|
|
uint32_t crc;
|
|
|
|
uint32_t origcrc;
|
|
|
|
blkcnt_t lastlba;
|
|
|
|
|
|
|
|
/* Check the GPT header signature */
|
|
|
|
|
|
|
|
if (le64toh(gpt->signature) != GPT_HEADER_SIGNATURE)
|
|
|
|
{
|
|
|
|
ferr("GUID Partition Table Header signature is wrong:"
|
2022-01-13 07:54:30 +01:00
|
|
|
"0x%" PRIx64 " != 0x%llx\n",
|
2021-10-11 04:48:44 +02:00
|
|
|
le64toh(gpt->signature), GPT_HEADER_SIGNATURE);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Check the GUID Partition Table CRC */
|
|
|
|
|
|
|
|
origcrc = gpt->header_crc32;
|
|
|
|
gpt->header_crc32 = 0;
|
|
|
|
crc = crc32part((FAR const uint8_t *)gpt,
|
|
|
|
le32toh(gpt->header_size), ~0l) ^ ~0l;
|
|
|
|
if (crc != le32toh(origcrc))
|
|
|
|
{
|
|
|
|
ferr("GUID Partition Table Header CRC is wrong: %" PRIx32
|
|
|
|
" != %" PRIx32 "\n", crc, le32toh(origcrc));
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
gpt->header_crc32 = origcrc;
|
|
|
|
|
|
|
|
/* Check that the my_lba entry points to the LBA that contains
|
|
|
|
* the GUID Partition Table
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (le64toh(gpt->my_lba) != lba)
|
|
|
|
{
|
2022-07-16 15:35:25 +02:00
|
|
|
ferr("GPT: my_lba incorrect: %" PRIx64 " != %" PRIxOFF "\n",
|
2021-10-11 04:48:44 +02:00
|
|
|
le64toh(gpt->my_lba), lba);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Check the first_usable_lba and last_usable_lba are within the disk. */
|
|
|
|
|
|
|
|
lastlba = gpt_last_lba(state);
|
|
|
|
if (le64toh(gpt->first_usable_lba) > lastlba)
|
|
|
|
{
|
2022-07-16 15:35:25 +02:00
|
|
|
ferr("GPT: first_usable_lba incorrect: %" PRId64 " > %" PRIdOFF "\n",
|
2021-10-11 04:48:44 +02:00
|
|
|
le64toh(gpt->first_usable_lba), lastlba);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (le64toh(gpt->last_usable_lba) > lastlba)
|
|
|
|
{
|
2022-07-16 15:35:25 +02:00
|
|
|
ferr("GPT: last_usable_lba incorrect: %" PRId64 " > %" PRIdOFF "\n",
|
2021-10-11 04:48:44 +02:00
|
|
|
le64toh(gpt->last_usable_lba), lastlba);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
return OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Name: gpt_pte_is_valid()
|
|
|
|
*
|
|
|
|
* Description:
|
|
|
|
* tests one PTE for validity
|
|
|
|
*
|
|
|
|
* Input Parameters:
|
|
|
|
* pte is the pte to check
|
|
|
|
* lastlba is last lba of the disk
|
|
|
|
*
|
|
|
|
* Returned Value:
|
|
|
|
* Returns 1 if valid, 0 on error.
|
|
|
|
*
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
static inline int gpt_pte_is_valid(FAR const struct gpt_entry_s *pte,
|
|
|
|
blkcnt_t lastlba)
|
|
|
|
{
|
|
|
|
if (!memcmp(&pte->partition_type_guid, &g_null_guid,
|
|
|
|
sizeof(g_null_guid)) ||
|
|
|
|
le64toh(pte->starting_lba) > lastlba ||
|
|
|
|
le64toh(pte->ending_lba) > lastlba)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gpt_part_set_name(FAR struct gpt_entry_s *pte,
|
|
|
|
FAR char *dest, size_t len)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
if (--len > GPT_PARTNAME_MAX_SIZE)
|
|
|
|
{
|
|
|
|
len = GPT_PARTNAME_MAX_SIZE;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i = 0; i < len; i++)
|
|
|
|
{
|
|
|
|
uint8_t c = pte->partition_name[i];
|
|
|
|
dest[i] = (c && !isprint(c)) ? '.' : c;
|
|
|
|
}
|
|
|
|
|
|
|
|
dest[i] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Public Functions
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
|
|
* Name: parse_gpt_partition
|
|
|
|
*
|
|
|
|
* Description:
|
|
|
|
* parse the gpt(EFI GUID Partition Table) partition.
|
|
|
|
*
|
|
|
|
* Input Parameters:
|
|
|
|
* state - The partition table state
|
|
|
|
* handler - The function to be called for each found partition
|
|
|
|
* arg - A caller provided value to return with the handler
|
|
|
|
*
|
|
|
|
* Returned Value:
|
|
|
|
* Zero on success; A negated errno value is returned on a failure
|
|
|
|
*
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
int parse_gpt_partition(FAR struct partition_state_s *state,
|
|
|
|
partition_handler_t handler,
|
|
|
|
FAR void *arg)
|
|
|
|
{
|
|
|
|
FAR struct legacy_partition_s *pmbr;
|
|
|
|
FAR struct gpt_ptable_s *ptbl;
|
|
|
|
FAR struct gpt_header_s *gpt;
|
|
|
|
FAR struct gpt_entry_s *ptes;
|
|
|
|
struct partition_s pentry;
|
2022-09-26 13:44:07 +02:00
|
|
|
blkcnt_t lastlba;
|
2021-10-11 04:48:44 +02:00
|
|
|
int nb_part;
|
|
|
|
int count;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
/* Read GPT Ptable (LBA0 + LBA1) */
|
|
|
|
|
|
|
|
count = (sizeof(struct gpt_ptable_s) + (state->blocksize - 1)) /
|
|
|
|
state->blocksize;
|
|
|
|
ptbl = kmm_malloc(count * state->blocksize);
|
|
|
|
if (!ptbl)
|
|
|
|
{
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = read_partition_block(state, ptbl, 0, count);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Verify mbr is valid */
|
|
|
|
|
|
|
|
pmbr = (FAR struct legacy_partition_s *)&ptbl->mbr[0x1be];
|
|
|
|
if (pmbr->sys_ind != 0xee)
|
|
|
|
{
|
|
|
|
ret = -EINVAL;
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Verify gpt header is valid */
|
|
|
|
|
|
|
|
gpt = &(ptbl->u.gpt_header);
|
|
|
|
ret = gpt_header_is_valid(state, gpt, 1);
|
|
|
|
if (ret >= 0)
|
|
|
|
{
|
|
|
|
/* Verify gpt header is valid */
|
|
|
|
|
|
|
|
ptes = gpt_alloc_verify_entries(state, gpt);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ret < 0 || !ptes)
|
|
|
|
{
|
|
|
|
/* Read and Verify backup gpt header is valid */
|
|
|
|
|
|
|
|
finfo("Primary GPT is invalid, using alternate GPT.\n");
|
|
|
|
|
|
|
|
count = (GPT_BLOCK_SIZE + state->blocksize - 1) / state->blocksize;
|
|
|
|
ret = read_partition_block(state, ptbl,
|
|
|
|
GPT_LBA_TO_BLOCK(gpt->alternate_lba,
|
|
|
|
state->blocksize), count);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
|
|
|
gpt = (FAR struct gpt_header_s *)ptbl;
|
|
|
|
ret = gpt_header_is_valid(state, gpt,
|
|
|
|
le64toh(gpt->alternate_lba));
|
|
|
|
if (ret >= 0)
|
|
|
|
{
|
|
|
|
/* Verify gpt header is valid */
|
|
|
|
|
|
|
|
ptes = gpt_alloc_verify_entries(state, gpt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ret < 0 || !ptes)
|
|
|
|
{
|
|
|
|
finfo("Alternate GPT is also invalid!!\n");
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
2022-09-26 13:44:07 +02:00
|
|
|
lastlba = gpt_last_lba(state);
|
2021-10-11 04:48:44 +02:00
|
|
|
nb_part = le32toh(gpt->num_partition_entries);
|
|
|
|
for (pentry.index = 0; pentry.index < nb_part; pentry.index++)
|
|
|
|
{
|
2022-09-26 13:44:07 +02:00
|
|
|
/* Skip the empty or invalid entries */
|
|
|
|
|
|
|
|
if (!gpt_pte_is_valid(&ptes[pentry.index], lastlba))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-10-11 04:48:44 +02:00
|
|
|
pentry.firstblock = GPT_LBA_TO_BLOCK(ptes[pentry.index].starting_lba,
|
|
|
|
state->blocksize);
|
2022-09-26 13:44:07 +02:00
|
|
|
pentry.nblocks = GPT_LBA_TO_BLOCK(ptes[pentry.index].ending_lba + 1,
|
2021-10-11 04:48:44 +02:00
|
|
|
state->blocksize) -
|
|
|
|
pentry.firstblock;
|
|
|
|
pentry.blocksize = state->blocksize;
|
|
|
|
gpt_part_set_name(&ptes[pentry.index], pentry.name,
|
|
|
|
sizeof(pentry.name));
|
|
|
|
handler(&pentry, arg);
|
|
|
|
}
|
|
|
|
|
|
|
|
kmm_free(ptes);
|
|
|
|
err:
|
|
|
|
kmm_free(ptbl);
|
|
|
|
return ret;
|
|
|
|
}
|