604 lines
15 KiB
C
604 lines
15 KiB
C
/* $NetBSD: mbr.c,v 1.14 1999/04/14 16:00:42 bouyer Exp $ */
|
|
|
|
/*
|
|
* Copyright 1997 Piermont Information Systems Inc.
|
|
* All rights reserved.
|
|
* * Copyright 1997 Piermont Information Systems Inc.
|
|
* All rights reserved.
|
|
*
|
|
* Written by Philip A. Nelson for Piermont Information Systems Inc.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. All advertising materials mentioning features or use of this software
|
|
* must display the following acknowledgement:
|
|
* This product includes software develooped for the NetBSD Project by
|
|
* Piermont Information Systems Inc.
|
|
* 4. The name of Piermont Information Systems Inc. may not be used to endorse
|
|
* or promote products derived from this software without specific prior
|
|
* written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY PIERMONT INFORMATION SYSTEMS INC. ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL PIERMONT INFORMATION SYSTEMS INC. BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Following applies to the geometry guessing code
|
|
*/
|
|
|
|
/*
|
|
* Mach Operating System
|
|
* Copyright (c) 1992 Carnegie Mellon University
|
|
* All Rights Reserved.
|
|
*
|
|
* Permission to use, copy, modify and distribute this software and its
|
|
* documentation is hereby granted, provided that both the copyright
|
|
* notice and this permission notice appear in all copies of the
|
|
* software, derivative works or modified versions, and any portions
|
|
* thereof, and that both notices appear in supporting documentation.
|
|
*
|
|
* CARNEGIE MELLON ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS"
|
|
* CONDITION. CARNEGIE MELLON DISCLAIMS ANY LIABILITY OF ANY KIND FOR
|
|
* ANY DAMAGES WHATSOEVER RESULTING FROM THE USE OF THIS SOFTWARE.
|
|
*
|
|
* Carnegie Mellon requests users of this software to return to
|
|
*
|
|
* Software Distribution Coordinator or Software.Distribution@CS.CMU.EDU
|
|
* School of Computer Science
|
|
* Carnegie Mellon University
|
|
* Pittsburgh PA 15213-3890
|
|
*
|
|
* any improvements or extensions that they make and grant Carnegie Mellon
|
|
* the rights to redistribute these changes.
|
|
*/
|
|
|
|
/* mbr.c -- DOS Master Boot Record editing code */
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/types.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <util.h>
|
|
#include "defs.h"
|
|
#include "mbr.h"
|
|
#include "md.h"
|
|
#include "msg_defs.h"
|
|
#include "menu_defs.h"
|
|
#include "endian.h"
|
|
|
|
struct part_id {
|
|
int id;
|
|
char *name;
|
|
} part_ids[] = {
|
|
{0, "unused"},
|
|
{MBR_PTYPE_FAT12, "Primary DOS, 12 bit FAT"},
|
|
{MBR_PTYPE_FAT16S, "Primary DOS, 16 bit FAT <32M"},
|
|
{MBR_PTYPE_EXT, "Extended DOS"},
|
|
{MBR_PTYPE_FAT16B, "Primary DOS, 16-bit FAT >32MB"},
|
|
{MBR_PTYPE_NTFS, "NTFS"},
|
|
{MBR_PTYPE_LNXSWAP, "Linux swap"},
|
|
{MBR_PTYPE_LNXEXT2, "Linux native"},
|
|
{MBR_PTYPE_386BSD, "old NetBSD/FreeBSD/386BSD"},
|
|
{MBR_PTYPE_NETBSD, "NetBSD"},
|
|
{-1, "Unknown"},
|
|
};
|
|
|
|
int dosptyp_nbsd = MBR_PTYPE_NETBSD;
|
|
|
|
static int get_mapping __P((struct mbr_partition *, int, int *, int *, int *,
|
|
long *absolute));
|
|
static void convert_mbr_chs __P((int, int, int, u_int8_t *, u_int8_t *,
|
|
u_int8_t *, u_int32_t));
|
|
|
|
|
|
/*
|
|
* First, geometry stuff...
|
|
*/
|
|
int
|
|
check_geom()
|
|
{
|
|
|
|
return bcyl <= 1024 && bsec < 64 && bcyl > 0 && bhead > 0 && bsec > 0;
|
|
}
|
|
|
|
/*
|
|
* get C/H/S geometry from user via menu interface and
|
|
* store in globals.
|
|
*/
|
|
void
|
|
set_bios_geom(cyl, head, sec)
|
|
int cyl, head, sec;
|
|
{
|
|
char res[80];
|
|
|
|
msg_display_add(MSG_setbiosgeom);
|
|
snprintf(res, 80, "%d", cyl);
|
|
msg_prompt_add(MSG_cylinders, res, res, 80);
|
|
bcyl = atoi(res);
|
|
|
|
snprintf(res, 80, "%d", head);
|
|
msg_prompt_add(MSG_heads, res, res, 80);
|
|
bhead = atoi(res);
|
|
|
|
snprintf(res, 80, "%d", sec);
|
|
msg_prompt_add(MSG_sectors, res, res, 80);
|
|
bsec = atoi(res);
|
|
}
|
|
|
|
void
|
|
disp_cur_geom()
|
|
{
|
|
|
|
msg_display_add(MSG_realgeom, dlcyl, dlhead, dlsec);
|
|
msg_display_add(MSG_biosgeom, bcyl, bhead, bsec);
|
|
}
|
|
|
|
|
|
/*
|
|
* Then, the partition stuff...
|
|
*/
|
|
int
|
|
otherpart(id)
|
|
int id;
|
|
{
|
|
|
|
return (id != 0 && id != MBR_PTYPE_386BSD && id != MBR_PTYPE_NETBSD);
|
|
}
|
|
|
|
int
|
|
ourpart(id)
|
|
int id;
|
|
{
|
|
|
|
return (id == MBR_PTYPE_386BSD || id == MBR_PTYPE_NETBSD);
|
|
}
|
|
|
|
/*
|
|
* Let user change incore Master Boot Record partitions via menu.
|
|
*/
|
|
int
|
|
edit_mbr(partition)
|
|
struct mbr_partition *partition;
|
|
{
|
|
int i, j;
|
|
|
|
/* Ask full/part */
|
|
|
|
/* XXX this sucks ("part" is used in menus, no param passing there) */
|
|
part = partition;
|
|
msg_display(MSG_fullpart, diskdev);
|
|
process_menu(MENU_fullpart);
|
|
|
|
/* DOS fdisk label checking and value setting. */
|
|
if (usefull) {
|
|
int otherparts = 0;
|
|
int ourparts = 0;
|
|
|
|
int i;
|
|
/* Count nonempty, non-BSD partitions. */
|
|
for (i = 0; i < NMBRPART; i++) {
|
|
otherparts += otherpart(part[i].mbrp_typ);
|
|
/* check for dualboot *bsd too */
|
|
ourparts += ourpart(part[i].mbrp_typ);
|
|
}
|
|
|
|
/* Ask if we really want to blow away non-NetBSD stuff */
|
|
if (otherparts != 0 || ourparts > 1) {
|
|
msg_display(MSG_ovrwrite);
|
|
process_menu(MENU_noyes);
|
|
if (!yesno) {
|
|
if (logging)
|
|
(void)fprintf(log, "User answered no to destroy other data, aborting.\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Set the partition information for full disk usage. */
|
|
part[0].mbrp_typ = part[0].mbrp_flag = 0;
|
|
part[0].mbrp_start = part[0].mbrp_size = 0;
|
|
part[1].mbrp_typ = part[0].mbrp_flag = 0;
|
|
part[1].mbrp_start = part[0].mbrp_size = 0;
|
|
part[2].mbrp_typ = part[0].mbrp_flag = 0;
|
|
part[2].mbrp_start = part[0].mbrp_size = 0;
|
|
part[3].mbrp_typ = dosptyp_nbsd;
|
|
part[3].mbrp_size = bsize - bsec;
|
|
part[3].mbrp_start = bsec;
|
|
part[3].mbrp_flag = 0x80;
|
|
|
|
ptstart = bsec;
|
|
ptsize = bsize - bsec;
|
|
fsdsize = dlsize;
|
|
fsptsize = dlsize - bsec;
|
|
fsdmb = fsdsize / MEG;
|
|
activepart = 3;
|
|
} else {
|
|
int numbsd, overlap;
|
|
int numfreebsd, freebsdpart; /* dual-boot */
|
|
|
|
/* Ask for sizes, which partitions, ... */
|
|
ask_sizemult();
|
|
bsdpart = freebsdpart = -1;
|
|
activepart = -1;
|
|
for (i = 0; i<4; i++)
|
|
if (part[i].mbrp_flag != 0) {
|
|
activepart = i;
|
|
part[i].mbrp_flag = 0;
|
|
}
|
|
do {
|
|
process_menu (MENU_editparttable);
|
|
numbsd = 0;
|
|
bsdpart = -1;
|
|
freebsdpart = -1;
|
|
numfreebsd = 0;
|
|
overlap = 0;
|
|
yesno = 0;
|
|
for (i=0; i<4; i++) {
|
|
/* Count 386bsd/FreeBSD/NetBSD(old) partitions */
|
|
if (part[i].mbrp_typ == MBR_PTYPE_386BSD) {
|
|
freebsdpart = i;
|
|
numfreebsd++;
|
|
}
|
|
/* Count NetBSD-only partitions */
|
|
if (part[i].mbrp_typ == MBR_PTYPE_NETBSD) {
|
|
bsdpart = i;
|
|
numbsd++;
|
|
}
|
|
for (j = i+1; j<4; j++)
|
|
if (partsoverlap(part, i,j))
|
|
overlap = 1;
|
|
}
|
|
|
|
/* If no new-NetBSD partition, use 386bsd instead */
|
|
if (numbsd == 0 && numfreebsd > 0) {
|
|
numbsd = numfreebsd;
|
|
bsdpart = freebsdpart;
|
|
/* XXX check partition type? */
|
|
}
|
|
|
|
/* Check for overlap or multiple native partitions */
|
|
if (overlap || numbsd != 1) {
|
|
msg_display(MSG_reeditpart);
|
|
process_menu(MENU_yesno);
|
|
}
|
|
} while (yesno && (numbsd != 1 || overlap));
|
|
|
|
if (activepart != -1)
|
|
part[activepart].mbrp_flag = 0x80;
|
|
|
|
if (numbsd == 0) {
|
|
msg_display(MSG_nobsdpart);
|
|
process_menu(MENU_ok);
|
|
return 0;
|
|
}
|
|
|
|
if (numbsd > 1) {
|
|
msg_display(MSG_multbsdpart, bsdpart);
|
|
process_menu(MENU_ok);
|
|
}
|
|
|
|
ptstart = part[bsdpart].mbrp_start;
|
|
ptsize = part[bsdpart].mbrp_size;
|
|
fsdsize = dlsize;
|
|
if (ptstart + ptsize < bsize)
|
|
fsptsize = ptsize;
|
|
else
|
|
fsptsize = dlsize - ptstart;
|
|
fsdmb = fsdsize / MEG;
|
|
|
|
/* Ask if a boot selector is wanted. XXXX */
|
|
}
|
|
|
|
/* Compute minimum NetBSD partition sizes (in sectors). */
|
|
minfsdmb = (80 + 4*rammb) * (MEG / sectorsize);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
partsoverlap(part, i, j)
|
|
struct mbr_partition *part;
|
|
int i;
|
|
int j;
|
|
{
|
|
|
|
if (part[i].mbrp_size == 0 || part[j].mbrp_size == 0)
|
|
return 0;
|
|
|
|
return
|
|
(part[i].mbrp_start < part[j].mbrp_start &&
|
|
part[i].mbrp_start + part[i].mbrp_size > part[j].mbrp_start)
|
|
||
|
|
(part[i].mbrp_start > part[j].mbrp_start &&
|
|
part[i].mbrp_start < part[j].mbrp_start + part[j].mbrp_size)
|
|
||
|
|
(part[i].mbrp_start == part[j].mbrp_start);
|
|
}
|
|
|
|
void
|
|
disp_cur_part(part, sel, disp)
|
|
struct mbr_partition *part;
|
|
int sel;
|
|
int disp;
|
|
{
|
|
int i, j, start, stop, rsize, rend;
|
|
|
|
if (disp < 0)
|
|
start = 0, stop = 4;
|
|
else
|
|
start = disp, stop = disp+1;
|
|
msg_display_add(MSG_part_head, multname, multname, multname);
|
|
for (i = start; i < stop; i++) {
|
|
if (sel == i)
|
|
msg_standout();
|
|
if (part[i].mbrp_size == 0 && part[i].mbrp_start == 0)
|
|
msg_printf_add("%d %36s ", i, "");
|
|
else {
|
|
rsize = part[i].mbrp_size / sizemult;
|
|
if (part[i].mbrp_size % sizemult)
|
|
rsize++;
|
|
rend = (part[i].mbrp_start + part[i].mbrp_size) / sizemult;
|
|
if ((part[i].mbrp_size + part[i].mbrp_size) % sizemult)
|
|
rend++;
|
|
msg_printf_add("%d %12d%12d%12d ", i,
|
|
part[i].mbrp_start / sizemult, rsize, rend);
|
|
}
|
|
for (j = 0; part_ids[j].id != -1 &&
|
|
part_ids[j].id != part[i].mbrp_typ; j++);
|
|
msg_printf_add("%s\n", part_ids[j].name);
|
|
if (sel == i)
|
|
msg_standend();
|
|
}
|
|
}
|
|
|
|
int
|
|
read_mbr(disk, buf, len)
|
|
char *disk, *buf;
|
|
int len;
|
|
{
|
|
char diskpath[MAXPATHLEN];
|
|
int fd, i;
|
|
struct mbr_partition *mbrp;
|
|
|
|
/* Open the disk. */
|
|
fd = opendisk(disk, O_RDONLY, diskpath, sizeof(diskpath), 0);
|
|
if (fd < 0)
|
|
return -1;
|
|
|
|
if (lseek(fd, MBR_BBSECTOR * MBR_SECSIZE, SEEK_SET) < 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
if (read(fd, buf, len) < len) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
if (valid_mbr(buf)) {
|
|
mbrp = (struct mbr_partition *)&buf[MBR_PARTOFF];
|
|
for (i = 0; i < NMBRPART; i++) {
|
|
if (mbrp[i].mbrp_typ != 0) {
|
|
mbrp[i].mbrp_start =
|
|
le_to_native32(mbrp[i].mbrp_start);
|
|
mbrp[i].mbrp_size =
|
|
le_to_native32(mbrp[i].mbrp_size);
|
|
}
|
|
}
|
|
}
|
|
|
|
(void)close(fd);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
write_mbr(disk, buf, len)
|
|
char *disk, *buf;
|
|
int len;
|
|
{
|
|
char diskpath[MAXPATHLEN];
|
|
int fd, i, ret = 0;
|
|
struct mbr_partition *mbrp;
|
|
u_int32_t pstart, psize;
|
|
|
|
/* Open the disk. */
|
|
fd = opendisk(disk, O_WRONLY, diskpath, sizeof(diskpath), 0);
|
|
if (fd < 0)
|
|
return -1;
|
|
|
|
if (lseek(fd, MBR_BBSECTOR * MBR_SECSIZE, SEEK_SET) < 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
mbrp = (struct mbr_partition *)&buf[MBR_PARTOFF];
|
|
for (i = 0; i < NMBRPART; i++) {
|
|
if (mbrp[i].mbrp_start == 0 &&
|
|
mbrp[i].mbrp_size == 0) {
|
|
mbrp[i].mbrp_scyl = 0;
|
|
mbrp[i].mbrp_shd = 0;
|
|
mbrp[i].mbrp_ssect = 0;
|
|
mbrp[i].mbrp_ecyl = 0;
|
|
mbrp[i].mbrp_ehd = 0;
|
|
mbrp[i].mbrp_esect = 0;
|
|
} else {
|
|
pstart = mbrp[i].mbrp_start;
|
|
psize = mbrp[i].mbrp_size;
|
|
mbrp[i].mbrp_start = native_to_le32(pstart);
|
|
mbrp[i].mbrp_size = native_to_le32(psize);
|
|
convert_mbr_chs(bcyl, bhead, bsec,
|
|
&mbrp[i].mbrp_scyl, &mbrp[i].mbrp_shd,
|
|
&mbrp[i].mbrp_ssect, pstart);
|
|
convert_mbr_chs(bcyl, bhead, bsec,
|
|
&mbrp[i].mbrp_ecyl, &mbrp[i].mbrp_ehd,
|
|
&mbrp[i].mbrp_esect, pstart + psize);
|
|
}
|
|
}
|
|
|
|
if (write(fd, buf, len) < 0)
|
|
ret = -1;
|
|
|
|
(void)close(fd);
|
|
return ret;
|
|
}
|
|
|
|
int
|
|
valid_mbr(buf)
|
|
char *buf;
|
|
{
|
|
u_int16_t magic;
|
|
|
|
magic = *((u_int16_t *)&buf[MBR_MAGICOFF]);
|
|
|
|
return (le_to_native16(magic) == MBR_MAGIC);
|
|
}
|
|
|
|
static void
|
|
convert_mbr_chs(cyl, head, sec, cylp, headp, secp, relsecs)
|
|
int cyl, head, sec;
|
|
u_int8_t *cylp, *headp, *secp;
|
|
u_int32_t relsecs;
|
|
{
|
|
unsigned int tcyl, temp, thead, tsec;
|
|
|
|
temp = head * sec;
|
|
tcyl = relsecs / temp;
|
|
|
|
if (tcyl >= 1024) {
|
|
*cylp = *headp = *secp = 0xff;
|
|
return;
|
|
}
|
|
|
|
relsecs %= temp;
|
|
thead = relsecs / sec;
|
|
|
|
tsec = (relsecs % sec) + 1;
|
|
|
|
*cylp = MBR_PUT_LSCYL(tcyl);
|
|
*headp = thead;
|
|
*secp = MBR_PUT_MSCYLANDSEC(tcyl, tsec);
|
|
}
|
|
|
|
/*
|
|
* This function is ONLY to be used as a last resort to provide a
|
|
* hint for the user. Ports should provide a more reliable way
|
|
* of getting the BIOS geometry. The i386 code, for example,
|
|
* uses the BIOS geometry as passed on from the bootblocks,
|
|
* and only uses this as a hint to the user when that information
|
|
* is not present, or a match could not be made with a NetBSD
|
|
* device.
|
|
*/
|
|
int
|
|
guess_biosgeom_from_mbr(buf, cyl, head, sec)
|
|
char *buf;
|
|
int *cyl, *head, *sec;
|
|
{
|
|
struct mbr_partition *parts = (struct mbr_partition *)&buf[MBR_PARTOFF];
|
|
int cylinders = -1, heads = -1, sectors = -1, i, j;
|
|
int c1, h1, s1, c2, h2, s2;
|
|
long a1, a2;
|
|
quad_t num, denom;
|
|
|
|
*cyl = *head = *sec = -1;
|
|
|
|
/* Try to deduce the number of heads from two different mappings. */
|
|
for (i = 0; i < NMBRPART * 2; i++) {
|
|
if (get_mapping(parts, i, &c1, &h1, &s1, &a1) < 0)
|
|
continue;
|
|
for (j = 0; j < 8; j++) {
|
|
if (get_mapping(parts, j, &c2, &h2, &s2, &a2) < 0)
|
|
continue;
|
|
num = (quad_t)h1*(a2-s2) - (quad_t)h2*(a1-s1);
|
|
denom = (quad_t)c2*(a1-s1) - (quad_t)c1*(a2-s2);
|
|
if (denom != 0 && num % denom == 0) {
|
|
heads = num / denom;
|
|
break;
|
|
}
|
|
}
|
|
if (heads != -1)
|
|
break;
|
|
}
|
|
|
|
if (heads == -1)
|
|
return -1;
|
|
|
|
/* Now figure out the number of sectors from a single mapping. */
|
|
for (i = 0; i < NMBRPART * 2; i++) {
|
|
if (get_mapping(parts, i, &c1, &h1, &s1, &a1) < 0)
|
|
continue;
|
|
num = a1 - s1;
|
|
denom = c1 * heads + h1;
|
|
if (denom != 0 && num % denom == 0) {
|
|
sectors = num / denom;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sectors == -1)
|
|
return -1;
|
|
|
|
/*
|
|
* Estimate the number of cylinders.
|
|
* XXX relies on get_disks having been called.
|
|
*/
|
|
cylinders = disk->dd_totsec / heads / sectors;
|
|
|
|
/* Now verify consistency with each of the partition table entries.
|
|
* Be willing to shove cylinders up a little bit to make things work,
|
|
* but translation mismatches are fatal. */
|
|
for (i = 0; i < NMBRPART * 2; i++) {
|
|
if (get_mapping(parts, i, &c1, &h1, &s1, &a1) < 0)
|
|
continue;
|
|
if (sectors * (c1 * heads + h1) + s1 != a1)
|
|
return -1;
|
|
if (c1 >= cylinders)
|
|
cylinders = c1 + 1;
|
|
}
|
|
|
|
/* Everything checks out. Reset the geometry to use for further
|
|
* calculations. */
|
|
*cyl = cylinders;
|
|
*head = heads;
|
|
*sec = sectors;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
get_mapping(parts, i, cylinder, head, sector, absolute)
|
|
struct mbr_partition *parts;
|
|
int i, *cylinder, *head, *sector;
|
|
long *absolute;
|
|
{
|
|
struct mbr_partition *part = &parts[i / 2];
|
|
|
|
if (part->mbrp_typ == 0)
|
|
return -1;
|
|
if (i % 2 == 0) {
|
|
*cylinder = MBR_PCYL(part->mbrp_scyl, part->mbrp_ssect);
|
|
*head = part->mbrp_shd;
|
|
*sector = MBR_PSECT(part->mbrp_ssect) - 1;
|
|
*absolute = part->mbrp_start;
|
|
} else {
|
|
*cylinder = MBR_PCYL(part->mbrp_ecyl, part->mbrp_esect);
|
|
*head = part->mbrp_ehd;
|
|
*sector = MBR_PSECT(part->mbrp_esect) - 1;
|
|
*absolute = part->mbrp_start + part->mbrp_size - 1;
|
|
}
|
|
return 0;
|
|
}
|