2017-10-25 16:22:00 +00:00
|
|
|
/*
|
2018-02-26 13:06:10 +00:00
|
|
|
* This file is part of the TREZOR project, https://trezor.io/
|
2017-10-25 16:22:00 +00:00
|
|
|
*
|
2018-02-26 13:06:10 +00:00
|
|
|
* Copyright (c) SatoshiLabs
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2017-10-25 16:22:00 +00:00
|
|
|
*/
|
|
|
|
|
2017-10-24 16:16:36 +00:00
|
|
|
#include <string.h>
|
2017-10-25 16:22:00 +00:00
|
|
|
#include <stdlib.h>
|
2017-10-24 16:16:36 +00:00
|
|
|
#include <sys/types.h>
|
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
#include STM32_HAL_H
|
|
|
|
|
2017-10-24 16:16:36 +00:00
|
|
|
#include "common.h"
|
|
|
|
#include "display.h"
|
|
|
|
#include "flash.h"
|
2017-10-26 22:41:22 +00:00
|
|
|
#include "mini_printf.h"
|
2017-10-24 16:16:36 +00:00
|
|
|
#include "rng.h"
|
2017-10-25 16:22:00 +00:00
|
|
|
#include "sbu.h"
|
2017-10-24 16:16:36 +00:00
|
|
|
#include "sdcard.h"
|
2017-10-26 22:41:22 +00:00
|
|
|
#include "secbool.h"
|
2017-10-24 16:16:36 +00:00
|
|
|
#include "touch.h"
|
|
|
|
#include "usb.h"
|
2017-10-25 16:22:00 +00:00
|
|
|
|
2019-01-23 16:16:46 +00:00
|
|
|
#include "memzero.h"
|
|
|
|
|
2017-10-26 11:26:16 +00:00
|
|
|
enum { VCP_IFACE = 0x00 };
|
2017-10-25 16:22:00 +00:00
|
|
|
|
|
|
|
static void vcp_intr(void)
|
|
|
|
{
|
|
|
|
display_clear();
|
2017-12-07 14:31:23 +00:00
|
|
|
ensure(secfalse, "vcp_intr");
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void vcp_puts(const char *s, size_t len)
|
|
|
|
{
|
2018-01-01 20:48:39 +00:00
|
|
|
int r = usb_vcp_write_blocking(VCP_IFACE, (const uint8_t *) s, len, -1);
|
|
|
|
(void)r;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static char vcp_getchar(void)
|
|
|
|
{
|
2017-10-26 11:26:16 +00:00
|
|
|
uint8_t c = 0;
|
2018-01-01 20:48:39 +00:00
|
|
|
int r = usb_vcp_read_blocking(VCP_IFACE, &c, 1, -1);
|
|
|
|
(void)r;
|
|
|
|
return (char)c;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void vcp_readline(char *buf, size_t len)
|
|
|
|
{
|
|
|
|
for (;;) {
|
|
|
|
char c = vcp_getchar();
|
|
|
|
if (c == '\r') {
|
|
|
|
vcp_puts("\r\n", 2);
|
|
|
|
break;
|
|
|
|
}
|
2017-10-26 11:26:16 +00:00
|
|
|
if (c < 32 || c > 126) { // not printable
|
2017-10-25 16:22:00 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (len > 1) { // leave space for \0
|
|
|
|
*buf = c;
|
|
|
|
buf++;
|
|
|
|
len--;
|
2017-10-26 11:26:16 +00:00
|
|
|
vcp_puts(&c, 1);
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (len > 0) {
|
|
|
|
*buf = '\0';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void vcp_printf(const char *fmt, ...)
|
|
|
|
{
|
|
|
|
static char buf[128];
|
2017-10-26 11:26:16 +00:00
|
|
|
va_list va;
|
|
|
|
va_start(va, fmt);
|
|
|
|
int r = mini_vsnprintf(buf, sizeof(buf), fmt, va);
|
|
|
|
va_end(va);
|
2017-10-25 16:22:00 +00:00
|
|
|
vcp_puts(buf, r);
|
|
|
|
vcp_puts("\r\n", 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void usb_init_all(void)
|
|
|
|
{
|
|
|
|
enum {
|
|
|
|
VCP_PACKET_LEN = 64,
|
|
|
|
VCP_BUFFER_LEN = 1024,
|
|
|
|
};
|
|
|
|
|
|
|
|
static const usb_dev_info_t dev_info = {
|
2018-01-31 15:38:59 +00:00
|
|
|
.device_class = 0xEF, // Composite Device Class
|
|
|
|
.device_subclass = 0x02, // Common Class
|
|
|
|
.device_protocol = 0x01, // Interface Association Descriptor
|
|
|
|
.vendor_id = 0x1209,
|
|
|
|
.product_id = 0x53C1,
|
2018-02-02 16:41:30 +00:00
|
|
|
.release_num = 0x0400,
|
2018-01-31 15:38:59 +00:00
|
|
|
.manufacturer = "SatoshiLabs",
|
|
|
|
.product = "TREZOR",
|
|
|
|
.serial_number = "000000000000",
|
|
|
|
.interface = "TREZOR Interface",
|
2018-02-02 15:02:37 +00:00
|
|
|
.usb21_enabled = secfalse,
|
2018-06-04 11:50:23 +00:00
|
|
|
.usb21_landing = secfalse,
|
2017-10-25 16:22:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static uint8_t tx_packet[VCP_PACKET_LEN];
|
|
|
|
static uint8_t tx_buffer[VCP_BUFFER_LEN];
|
|
|
|
static uint8_t rx_packet[VCP_PACKET_LEN];
|
|
|
|
static uint8_t rx_buffer[VCP_BUFFER_LEN];
|
|
|
|
|
|
|
|
static const usb_vcp_info_t vcp_info = {
|
|
|
|
.tx_packet = tx_packet,
|
|
|
|
.tx_buffer = tx_buffer,
|
|
|
|
.rx_packet = rx_packet,
|
|
|
|
.rx_buffer = rx_buffer,
|
|
|
|
.tx_buffer_len = VCP_BUFFER_LEN,
|
|
|
|
.rx_buffer_len = VCP_BUFFER_LEN,
|
|
|
|
.rx_intr_fn = vcp_intr,
|
2017-10-26 11:26:16 +00:00
|
|
|
.rx_intr_byte = 3, // Ctrl-C
|
2017-10-25 16:22:00 +00:00
|
|
|
.iface_num = VCP_IFACE,
|
|
|
|
.data_iface_num = 0x01,
|
|
|
|
.ep_cmd = 0x82,
|
|
|
|
.ep_in = 0x81,
|
|
|
|
.ep_out = 0x01,
|
|
|
|
.polling_interval = 10,
|
|
|
|
.max_packet_len = VCP_PACKET_LEN,
|
|
|
|
};
|
|
|
|
|
|
|
|
usb_init(&dev_info);
|
|
|
|
ensure(usb_vcp_add(&vcp_info), "usb_vcp_add");
|
|
|
|
usb_start();
|
|
|
|
}
|
|
|
|
|
2018-01-09 20:21:36 +00:00
|
|
|
static void draw_border(int width, int padding)
|
2017-11-27 12:14:09 +00:00
|
|
|
{
|
2018-01-09 20:21:36 +00:00
|
|
|
const int W = width, P = padding, RX = DISPLAY_RESX, RY = DISPLAY_RESY;
|
2017-11-27 12:14:09 +00:00
|
|
|
display_clear();
|
2018-01-09 20:21:36 +00:00
|
|
|
display_bar(P, P, RX - 2 * P, RY - 2 * P, 0xFFFF);
|
|
|
|
display_bar(P + W, P + W, RX - 2 * (P + W), RY - 2 * (P + W), 0x0000);
|
2017-11-27 12:14:09 +00:00
|
|
|
display_refresh();
|
2018-01-09 20:21:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void test_border(void)
|
|
|
|
{
|
|
|
|
draw_border(2, 0);
|
2017-11-27 12:14:09 +00:00
|
|
|
vcp_printf("OK");
|
|
|
|
}
|
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
static void test_display(const char *colors)
|
|
|
|
{
|
|
|
|
display_clear();
|
|
|
|
|
|
|
|
size_t l = strlen(colors);
|
2017-10-26 11:26:16 +00:00
|
|
|
size_t w = DISPLAY_RESX / l;
|
2017-10-25 16:22:00 +00:00
|
|
|
|
|
|
|
for (size_t i = 0; i < l; i++) {
|
2017-10-26 11:26:16 +00:00
|
|
|
uint16_t c = 0x0000; // black
|
2017-10-25 16:22:00 +00:00
|
|
|
switch (colors[i]) {
|
2017-10-27 04:01:22 +00:00
|
|
|
case 'R': c = 0xF800; break;
|
|
|
|
case 'G': c = 0x07E0; break;
|
|
|
|
case 'B': c = 0x001F; break;
|
|
|
|
case 'W': c = 0xFFFF; break;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
display_bar(i * w, 0, i * w + w, 240, c);
|
|
|
|
}
|
|
|
|
display_refresh();
|
|
|
|
vcp_printf("OK");
|
|
|
|
}
|
|
|
|
|
2017-10-26 22:41:22 +00:00
|
|
|
static secbool touch_click_timeout(uint32_t *touch, uint32_t timeout_ms)
|
2017-10-25 16:22:00 +00:00
|
|
|
{
|
|
|
|
uint32_t deadline = HAL_GetTick() + timeout_ms;
|
2017-10-26 11:26:16 +00:00
|
|
|
uint32_t r = 0;
|
2017-10-25 16:22:00 +00:00
|
|
|
|
2017-10-26 11:26:16 +00:00
|
|
|
while (touch_read());
|
2017-10-25 16:22:00 +00:00
|
|
|
while ((touch_read() & TOUCH_START) == 0) {
|
2017-10-26 22:41:22 +00:00
|
|
|
if (HAL_GetTick() > deadline) return secfalse;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
while (((r = touch_read()) & TOUCH_END) == 0) {
|
2017-10-26 22:41:22 +00:00
|
|
|
if (HAL_GetTick() > deadline) return secfalse;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
2017-10-26 11:26:16 +00:00
|
|
|
while (touch_read());
|
2017-10-25 16:22:00 +00:00
|
|
|
|
|
|
|
*touch = r;
|
2017-10-26 22:41:22 +00:00
|
|
|
return sectrue;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void test_touch(const char *args)
|
|
|
|
{
|
|
|
|
int column = args[0] - '0';
|
|
|
|
int timeout = args[1] - '0';
|
|
|
|
|
|
|
|
display_clear();
|
|
|
|
switch (column) {
|
2017-10-27 15:26:14 +00:00
|
|
|
case 1: display_bar(0, 0, 120, 120, 0xFFFF); break;
|
|
|
|
case 2: display_bar(120, 0, 120, 120, 0xFFFF); break;
|
|
|
|
case 3: display_bar(120, 120, 120, 120, 0xFFFF); break;
|
|
|
|
default: display_bar(0, 120, 120, 120, 0xFFFF); break;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
display_refresh();
|
|
|
|
|
2018-07-18 22:01:54 +00:00
|
|
|
touch_power_on();
|
|
|
|
|
2017-10-26 22:41:22 +00:00
|
|
|
uint32_t evt = 0;
|
|
|
|
if (touch_click_timeout(&evt, timeout * 1000)) {
|
2018-10-02 15:24:03 +00:00
|
|
|
uint16_t x = touch_unpack_x(evt);
|
|
|
|
uint16_t y = touch_unpack_y(evt);
|
2017-10-26 11:26:16 +00:00
|
|
|
vcp_printf("OK %d %d", x, y);
|
2017-10-25 16:22:00 +00:00
|
|
|
} else {
|
|
|
|
vcp_printf("ERROR TIMEOUT");
|
|
|
|
}
|
|
|
|
display_clear();
|
|
|
|
display_refresh();
|
2018-07-18 22:01:54 +00:00
|
|
|
|
|
|
|
touch_power_off();
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void test_pwm(const char *args)
|
|
|
|
{
|
|
|
|
int v = atoi(args);
|
2017-10-26 11:26:16 +00:00
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
display_backlight(v);
|
|
|
|
display_refresh();
|
|
|
|
vcp_printf("OK");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void test_sd(void)
|
|
|
|
{
|
2017-10-29 14:07:34 +00:00
|
|
|
#define BLOCK_SIZE (32 * 1024)
|
2017-10-29 16:51:16 +00:00
|
|
|
static uint32_t buf1[BLOCK_SIZE / sizeof(uint32_t)];
|
|
|
|
static uint32_t buf2[BLOCK_SIZE / sizeof(uint32_t)];
|
2017-10-26 11:26:16 +00:00
|
|
|
|
2017-10-27 01:32:26 +00:00
|
|
|
if (sectrue != sdcard_is_present()) {
|
2017-10-25 16:22:00 +00:00
|
|
|
vcp_printf("ERROR NOCARD");
|
|
|
|
return;
|
|
|
|
}
|
2017-10-26 11:26:16 +00:00
|
|
|
|
2017-12-16 16:54:04 +00:00
|
|
|
ensure(sdcard_power_on(), NULL);
|
2017-10-29 14:07:34 +00:00
|
|
|
if (sectrue != sdcard_read_blocks(buf1, 0, BLOCK_SIZE / SDCARD_BLOCK_SIZE)) {
|
|
|
|
vcp_printf("ERROR sdcard_read_blocks (0)");
|
2017-10-26 11:26:16 +00:00
|
|
|
goto power_off;
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
2017-10-29 14:07:34 +00:00
|
|
|
for (int j = 1; j <= 2; j++) {
|
2017-10-29 16:51:16 +00:00
|
|
|
for (int i = 0; i < BLOCK_SIZE / sizeof(uint32_t); i++) {
|
|
|
|
buf1[i] ^= 0xFFFFFFFF;
|
2017-10-29 14:07:34 +00:00
|
|
|
}
|
|
|
|
if (sectrue != sdcard_write_blocks(buf1, 0, BLOCK_SIZE / SDCARD_BLOCK_SIZE)) {
|
|
|
|
vcp_printf("ERROR sdcard_write_blocks (%d)", j);
|
|
|
|
goto power_off;
|
|
|
|
}
|
2018-07-18 22:01:54 +00:00
|
|
|
HAL_Delay(1000);
|
2017-10-29 14:07:34 +00:00
|
|
|
if (sectrue != sdcard_read_blocks(buf2, 0, BLOCK_SIZE / SDCARD_BLOCK_SIZE)) {
|
|
|
|
vcp_printf("ERROR sdcard_read_blocks (%d)", j);
|
|
|
|
goto power_off;
|
|
|
|
}
|
|
|
|
if (0 != memcmp(buf1, buf2, sizeof(buf1))) {
|
|
|
|
vcp_printf("ERROR DATA MISMATCH");
|
|
|
|
goto power_off;
|
|
|
|
}
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
vcp_printf("OK");
|
2017-10-26 11:26:16 +00:00
|
|
|
|
|
|
|
power_off:
|
|
|
|
sdcard_power_off();
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
2018-04-28 13:48:39 +00:00
|
|
|
static void test_wipe(void)
|
|
|
|
{
|
|
|
|
// erase start of the firmware (metadata) -> invalidate FW
|
|
|
|
ensure(flash_unlock(), NULL);
|
|
|
|
for (int i = 0; i < 1024 / sizeof(uint32_t); i++) {
|
|
|
|
ensure(flash_write_word(FLASH_SECTOR_FIRMWARE_START, i * sizeof(uint32_t), 0x00000000), NULL);
|
|
|
|
}
|
|
|
|
ensure(flash_lock(), NULL);
|
|
|
|
display_clear();
|
|
|
|
display_text_center(DISPLAY_RESX / 2, DISPLAY_RESY / 2 + 10, "WIPED", -1, FONT_BOLD, COLOR_WHITE, COLOR_BLACK);
|
|
|
|
display_refresh();
|
|
|
|
vcp_printf("OK");
|
|
|
|
}
|
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
static void test_sbu(const char *args)
|
|
|
|
{
|
2017-10-26 22:41:22 +00:00
|
|
|
secbool sbu1 = sectrue * (args[0] == '1');
|
|
|
|
secbool sbu2 = sectrue * (args[1] == '1');
|
2017-10-25 16:22:00 +00:00
|
|
|
sbu_set(sbu1, sbu2);
|
|
|
|
vcp_printf("OK");
|
|
|
|
}
|
|
|
|
|
|
|
|
static void test_otp_read(void)
|
|
|
|
{
|
|
|
|
uint8_t data[32];
|
2019-01-23 16:16:46 +00:00
|
|
|
memzero(data, sizeof(data));
|
2017-12-16 16:54:04 +00:00
|
|
|
ensure(flash_otp_read(0, 0, data, sizeof(data)), NULL);
|
2017-10-30 13:08:56 +00:00
|
|
|
|
|
|
|
// strip trailing 0xFF
|
|
|
|
for (size_t i = 0; i < sizeof(data); i++) {
|
|
|
|
if (data[i] == 0xFF) {
|
|
|
|
data[i] = 0x00;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// use (null) for empty data
|
|
|
|
if (data[0] == 0x00) {
|
|
|
|
vcp_printf("OK (null)");
|
|
|
|
} else {
|
|
|
|
vcp_printf("OK %s", (const char *) data);
|
|
|
|
}
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void test_otp_write(const char *args)
|
|
|
|
{
|
|
|
|
char data[32];
|
2019-01-23 16:16:46 +00:00
|
|
|
memzero(data, sizeof(data));
|
2017-10-27 15:26:14 +00:00
|
|
|
strncpy(data, args, sizeof(data) - 1);
|
2017-12-16 16:54:04 +00:00
|
|
|
ensure(flash_otp_write(0, 0, (const uint8_t *) data, sizeof(data)), NULL);
|
|
|
|
ensure(flash_otp_lock(0), NULL);
|
2017-10-25 16:22:00 +00:00
|
|
|
vcp_printf("OK");
|
|
|
|
}
|
|
|
|
|
2017-10-26 22:41:22 +00:00
|
|
|
static secbool startswith(const char *s, const char *prefix)
|
2017-10-25 16:22:00 +00:00
|
|
|
{
|
2017-10-26 22:41:22 +00:00
|
|
|
return sectrue * (0 == strncmp(s, prefix, strlen(prefix)));
|
2017-10-25 16:22:00 +00:00
|
|
|
}
|
2017-10-24 16:16:36 +00:00
|
|
|
|
2017-10-27 04:01:22 +00:00
|
|
|
#define BACKLIGHT_NORMAL 150
|
|
|
|
|
2017-10-24 16:16:36 +00:00
|
|
|
int main(void)
|
|
|
|
{
|
|
|
|
display_orientation(0);
|
|
|
|
sdcard_init();
|
|
|
|
touch_init();
|
2017-10-25 16:22:00 +00:00
|
|
|
sbu_init();
|
|
|
|
usb_init_all();
|
|
|
|
|
|
|
|
display_clear();
|
2018-01-09 20:21:36 +00:00
|
|
|
draw_border(1, 3);
|
2017-10-24 16:16:36 +00:00
|
|
|
|
2017-10-27 04:01:22 +00:00
|
|
|
char dom[32];
|
|
|
|
// format: TREZOR2-YYMMDD
|
|
|
|
if (sectrue == flash_otp_read(0, 0, (uint8_t *)dom, 32) && 0 == memcmp(dom, "TREZOR2-", 8) && dom[31] == 0) {
|
|
|
|
display_qrcode(DISPLAY_RESX / 2, DISPLAY_RESY / 2, dom, strlen(dom), 4);
|
2018-02-06 21:54:46 +00:00
|
|
|
display_text_center(DISPLAY_RESX / 2, DISPLAY_RESY - 30, dom + 8, -1, FONT_BOLD, COLOR_WHITE, COLOR_BLACK);
|
2017-10-27 04:01:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
display_fade(0, BACKLIGHT_NORMAL, 1000);
|
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
char line[128];
|
2017-10-24 16:16:36 +00:00
|
|
|
|
|
|
|
for (;;) {
|
2017-10-25 16:22:00 +00:00
|
|
|
vcp_readline(line, sizeof(line));
|
|
|
|
|
|
|
|
if (startswith(line, "PING")) {
|
|
|
|
vcp_printf("OK");
|
|
|
|
|
2017-11-27 12:14:09 +00:00
|
|
|
} else if (startswith(line, "BORDER")) {
|
|
|
|
test_border();
|
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
} else if (startswith(line, "DISP ")) {
|
|
|
|
test_display(line + 5);
|
|
|
|
|
|
|
|
} else if (startswith(line, "TOUCH ")) {
|
|
|
|
test_touch(line + 6);
|
|
|
|
|
|
|
|
} else if (startswith(line, "PWM ")) {
|
|
|
|
test_pwm(line + 4);
|
|
|
|
|
|
|
|
} else if (startswith(line, "SD")) {
|
|
|
|
test_sd();
|
|
|
|
|
|
|
|
} else if (startswith(line, "SBU ")) {
|
|
|
|
test_sbu(line + 4);
|
|
|
|
|
|
|
|
} else if (startswith(line, "OTP READ")) {
|
|
|
|
test_otp_read();
|
|
|
|
|
|
|
|
} else if (startswith(line, "OTP WRITE ")) {
|
|
|
|
test_otp_write(line + 10);
|
2017-10-24 16:16:36 +00:00
|
|
|
|
2018-04-28 13:48:39 +00:00
|
|
|
} else if (startswith(line, "WIPE")) {
|
|
|
|
test_wipe();
|
|
|
|
|
2017-10-25 16:22:00 +00:00
|
|
|
} else {
|
|
|
|
vcp_printf("UNKNOWN");
|
|
|
|
}
|
2017-10-24 16:16:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|