From 72e5fc940ab60c5b3ea659a27033f74786c45323 Mon Sep 17 00:00:00 2001 From: Namyoon Woo Date: Sat, 7 Mar 2020 21:08:56 -0800 Subject: move ec_comm implementation to common directory This patch moves ec_comm.c and ec_efs.c from board/cr50 to common/, so that they can be shared with other board configuration (like host). This is to build unittest for those files. BUG=none BRANCH=cr50 TEST=make buildall -j Signed-off-by: Namyoon Woo Change-Id: I67ac313054ebe4604848a176f0a42e3483957e74 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/ec/+/2094076 Reviewed-by: Vadim Bendebury --- board/cr50/board.h | 2 + board/cr50/build.mk | 2 - board/cr50/ec_comm.c | 348 --------------------------------------------------- board/cr50/ec_comm.h | 45 ------- board/cr50/ec_efs.c | 257 ------------------------------------- 5 files changed, 2 insertions(+), 652 deletions(-) delete mode 100644 board/cr50/ec_comm.c delete mode 100644 board/cr50/ec_comm.h delete mode 100644 board/cr50/ec_efs.c (limited to 'board') diff --git a/board/cr50/board.h b/board/cr50/board.h index 25f8452503..1b93ca75ca 100644 --- a/board/cr50/board.h +++ b/board/cr50/board.h @@ -449,6 +449,8 @@ enum nvmem_users { #define CONFIG_FACTORY_MODE #define CONFIG_RNG +#define CONFIG_EC_EFS_SUPPORT + #define CONFIG_ENABLE_H1_ALERTS /* Enable hardware backed brute force resistance feature */ diff --git a/board/cr50/build.mk b/board/cr50/build.mk index 4b28c91641..5b0d788401 100644 --- a/board/cr50/build.mk +++ b/board/cr50/build.mk @@ -42,8 +42,6 @@ dirs-y += $(BDIR)/tpm2 board-y = board.o board-y += ap_state.o board-y += closed_source_set1.o -board-y += ec_comm.o -board-y += ec_efs.o board-y += ec_state.o board-y += power_button.o board-y += servo_state.o diff --git a/board/cr50/ec_comm.c b/board/cr50/ec_comm.c deleted file mode 100644 index c2e7bdb26d..0000000000 --- a/board/cr50/ec_comm.c +++ /dev/null @@ -1,348 +0,0 @@ -/* Copyright 2020 The Chromium OS Authors. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - * - * EC-CR50 communication - */ -#include "common.h" -#include "console.h" -#include "crc8.h" -#include "ec_comm.h" -#include "gpio.h" -#include "hooks.h" -#include "system.h" -#include "task.h" -#include "timer.h" -#include "uartn.h" -#include "usart.h" -#include "vboot.h" - -#define CPRINTS(format, args...) cprints(CC_TASK, "EC-COMM: " format, ## args) - -/* - * EC communications state machine (FSM) is supposed to be active between TPM - * reset (including power on) and the moment EC_IN_RW transition is latched. - * - * Each packet is supposed to be prepended by at least one synchronization - * character (0xEC). If packet does not verify, the entire stream contents, - * including all sync characters is delivered to the USB bridge. - * - * Meaning of the states included in the enum below is as follows: - */ -enum ec_comm_phase { - PHASE_READY_COMM = 0, - PHASE_RECEIVING_PREAMBLE, - PHASE_RECEIVING_HEADER, - PHASE_RECEIVING_DATA, -}; - -/* - * Context of EC-CR50 communication - */ -static struct ec_comm_context_ { - uint8_t uart; /* Current UART ID in packet mode. */ - /* UART_NULL if no UART is in packet mode */ - uint8_t phase; /* enum ec_comm_phase */ - uint8_t preamble_count; - uint8_t bytes_received; - - uint8_t bytes_expected; - uint8_t reserved[1]; - - uint16_t last_resp; - - union { - struct cr50_comm_packet ph; - uint8_t packet[CR50_COMM_MAX_PACKET_SIZE]; - }; -} ec_comm_ctx; - - -/* - * Initialize EC-CR50 communication context. - */ -static void ec_comm_init_(void) -{ - ec_comm_ctx.uart = UART_NULL; - - if (!board_has_ec_cr50_comm_support()) - return; - - CPRINTS("Initializtion"); - - gpio_enable_interrupt(GPIO_EC_PACKET_MODE_EN); - gpio_enable_interrupt(GPIO_EC_PACKET_MODE_DIS); - - /* If DIOB3 is already high, then enable the packet mode. */ - if (gpio_get_level(GPIO_EC_PACKET_MODE_EN)) - ec_comm_packet_mode_en(GPIO_EC_PACKET_MODE_EN); -} -DECLARE_HOOK(HOOK_INIT, ec_comm_init_, HOOK_PRIO_DEFAULT + 1); - -/* - * Process the received packet. - * - * @param ph Pointer to the received EC-Cr50 packet. - * @param bytes Total bytes of the received EC-Cr50 packet. - * @return CR50_COMM_SUCCESS if the packet has been processed successfully, - * CR50_COMM_ERROR_CRC if CRC is incorrect, - * CR50_COMM_ERROR_UNDEFINED_CMD if the cmd is unknown, - * CR50_COMM_ERROR_SIZE if data size is not as expected, or - * CR50_COMM_ERROR_BAD_PAYLOAD if the given hash and the hash in NVM - * are not same. - * 0 if it deosn't have to respond to EC. - */ -static uint16_t decode_packet_(const struct cr50_comm_packet *ph, int bytes) -{ - const int offset_cmd = offsetof(struct cr50_comm_packet, cmd); - uint8_t crc8_calc; - uint16_t response; - - /* Verify CRC. */ - crc8_calc = crc8((const uint8_t *)&ph->cmd, bytes - offset_cmd); - if (crc8_calc != ph->crc) - return CR50_COMM_ERROR_CRC; - - /* Execute the command. */ - switch (ph->cmd) { - case CR50_COMM_CMD_SET_BOOT_MODE: - response = ec_efs_set_boot_mode(ph->data, ph->size); - break; - - case CR50_COMM_CMD_VERIFY_HASH: - response = ec_efs_verify_hash(ph->data, ph->size); - break; - - default: - response = CR50_COMM_ERROR_UNDEFINED_CMD; - } - - return response; -} - -/* - * Transfer a 'response' to 'uart' port using DIOB3 pin. - * - * @param response Response code to return to EC. Should be one of - * CR50_COMM_RESPONSE codes in include/vboot.h. - */ -static void transfer_response_to_ec_(uint16_t response) -{ - uint8_t *ptr_resp = (uint8_t *)&response; - int uart = ec_comm_ctx.uart; - - /* Send the response to EC in little endian. */ - uartn_write_char(uart, ptr_resp[0]); - uartn_write_char(uart, ptr_resp[1]); - - uartn_tx_flush(uart); /* Flush from UART2_TX to DIOB3 */ - - ec_comm_ctx.last_resp = response; -} - -int ec_comm_process_packet(uint8_t ch) -{ - uint16_t response = 0; - - switch (ec_comm_ctx.phase) { - case PHASE_READY_COMM: - /* - * if it is not the preamble, then return 0 so that ch can be - * forwarded to USB. - */ - if (ch != CR50_COMM_PREAMBLE) { - ec_comm_ctx.preamble_count = 0; - return 0; - } - - /* - * Forward ch to USB even if it is CR50_COMM_PREAMBLE, because - * it is not yet sure whether it is a preamble or not. - * Forwarding 0xec to USB is not harmful anyway. - */ - if (++ec_comm_ctx.preamble_count < MIN_LENGTH_PREAMBLE) - return 0; - - ec_comm_ctx.phase = PHASE_RECEIVING_PREAMBLE; - break; - - case PHASE_RECEIVING_PREAMBLE: - if (ch == CR50_COMM_PREAMBLE) { - ++ec_comm_ctx.preamble_count; - break; - } - - /* - * First non-preamble character. Reset received bytes and - * fall through to receive header. - */ - ec_comm_ctx.bytes_received = 0; - ec_comm_ctx.bytes_expected = sizeof(struct cr50_comm_packet); - ec_comm_ctx.phase = PHASE_RECEIVING_HEADER; - /* FALLTHROUGH */ - - case PHASE_RECEIVING_HEADER: - /* - * Note: EC-CR50 communication is designed to perform in - * little endian. - */ - ec_comm_ctx.packet[ec_comm_ctx.bytes_received++] = ch; - - if (ec_comm_ctx.bytes_received < ec_comm_ctx.bytes_expected) - break; - - /* The header has been received. Let's parse it. */ - if (ec_comm_ctx.ph.magic != CR50_COMM_MAGIC_WORD) { - response = CR50_COMM_ERROR_MAGIC; - break; - } - - /* Check struct_version */ - /* - * Note: if CR50_COMM_VERSION gets bigger than 0x00, - * you should implement how to handle backward - * compatibility. - */ - if (ec_comm_ctx.ph.version != CR50_COMM_VERSION) { - response = CR50_COMM_ERROR_STRUCT_VERSION; - break; - } - - if (ec_comm_ctx.ph.size == 0) { - /* Data size zero, then process the packet now. */ - response = decode_packet_(&ec_comm_ctx.ph, - ec_comm_ctx.bytes_received); - } else if ((ec_comm_ctx.ph.size + ec_comm_ctx.bytes_expected) > - CR50_COMM_MAX_PACKET_SIZE) { - response = CR50_COMM_ERROR_SIZE; - } else { - ec_comm_ctx.bytes_expected += ec_comm_ctx.ph.size; - ec_comm_ctx.phase = PHASE_RECEIVING_DATA; - } - break; - - case PHASE_RECEIVING_DATA: - ec_comm_ctx.packet[ec_comm_ctx.bytes_received++] = ch; - - /* The EC is done sending the packet, let's process it. */ - if (ec_comm_ctx.bytes_received >= ec_comm_ctx.bytes_expected) - response = decode_packet_(&ec_comm_ctx.ph, - ec_comm_ctx.bytes_received); - break; - - default: - /* - * It is in the unknown phase. - * Let's turn the phase back to READY_COMM, and - * return 0 so that ch can be forwarded to USB. - */ - ec_comm_ctx.phase = PHASE_READY_COMM; - ec_comm_ctx.preamble_count = 0; - return 0; - } - - if (response) { - transfer_response_to_ec_(response); - -#ifdef CR50_RELAXED - CPRINTS("decoded a packet"); - CPRINTS("header : 0x%ph", - HEX_BUF((uint8_t *)&ec_comm_ctx.ph, - sizeof(struct cr50_comm_packet))); - CPRINTS("body : 0x%ph", - HEX_BUF((uint8_t *)ec_comm_ctx.ph.data, - ec_comm_ctx.ph.size)); - /* Let's response to EC */ - CPRINTS("response: 0x%04x", response); -#endif - /* - * If it reaches here, EC comm is either broken or one packet - * was well-processed. Let's turn the phase back to READY_COMM. - */ - ec_comm_ctx.phase = PHASE_READY_COMM; - ec_comm_ctx.preamble_count = 0; - } - - return 1; -} - -void ec_comm_packet_mode_en(enum gpio_signal unsed) -{ - disable_sleep(SLEEP_MASK_EC_CR50_COMM); - - /* Initialize packet context */ - ec_comm_ctx.phase = PHASE_READY_COMM; - ec_comm_ctx.preamble_count = 0; - ec_comm_ctx.uart = UART_EC; /* Enable Packet Mode */ - ccd_update_state(); -} - -void ec_comm_packet_mode_dis(enum gpio_signal unsed) -{ - ec_comm_ctx.uart = UART_NULL; /* Disable Packet Mode. */ - ccd_update_state(); - - enable_sleep(SLEEP_MASK_EC_CR50_COMM); -} - -int ec_comm_is_uart_in_packet_mode(int uart) -{ - return uart == ec_comm_ctx.uart; -} - -void ec_comm_block(int block) -{ - static int is_blocked; - - if (is_blocked == block) - return; - - if (block) { - gpio_disable_interrupt(GPIO_EC_PACKET_MODE_EN); - gpio_disable_interrupt(GPIO_EC_PACKET_MODE_DIS); - - if (ec_comm_is_uart_in_packet_mode(UART_EC)) - ec_comm_packet_mode_dis(GPIO_EC_PACKET_MODE_DIS); - } else { - gpio_enable_interrupt(GPIO_EC_PACKET_MODE_EN); - gpio_enable_interrupt(GPIO_EC_PACKET_MODE_DIS); - } - - is_blocked = block; - - /* Note: ccd_update_state() should be called to change UART status. */ -} - -/* - * A console command, printing EC-CR50-Comm status. - */ -static int command_ec_comm(int argc, char **argv) -{ - if (!board_has_ec_cr50_comm_support()) { - ccprintf("No EC-CR50 comm support\n"); - return EC_ERROR_INVAL; - } - - /* - * EC Packet Context - */ - ccprintf("uart : 0x%02x\n", ec_comm_ctx.uart); - ccprintf("packet mode : %sABLED\n", - ec_comm_is_uart_in_packet_mode(UART_EC) ? "EN" : "DIS"); - - ccprintf("phase : %d\n", ec_comm_ctx.phase); - ccprintf("preamble_count : %d\n", ec_comm_ctx.preamble_count); - ccprintf("bytes_received : %d\n", ec_comm_ctx.bytes_received); - ccprintf("bytes_expected : %d\n", ec_comm_ctx.bytes_expected); -#ifdef CR50_RELAXED - ccprintf("packet:\n"); - hexdump((uint8_t *)ec_comm_ctx.packet, CR50_COMM_MAX_PACKET_SIZE); -#endif /* CR50_RELAXED */ - ccprintf("response : 0x%04x\n", ec_comm_ctx.last_resp); - ccprintf("\n"); - ec_efs_print_status(); - - return EC_SUCCESS; -} -DECLARE_SAFE_CONSOLE_COMMAND(ec_comm, command_ec_comm, NULL, - "Dump EC-CR50-comm status"); diff --git a/board/cr50/ec_comm.h b/board/cr50/ec_comm.h deleted file mode 100644 index 263d311e63..0000000000 --- a/board/cr50/ec_comm.h +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright 2020 The Chromium OS Authors. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - */ -#ifndef __CROS_EC_COMM_H -#define __CROS_EC_COMM_H -#include -#include "common.h" -/* GPIO Interrupt handler for GPIO_EC_PACKET_MODE_EN rising edge */ -void ec_comm_packet_mode_en(enum gpio_signal unsed); -/* GPIO Interrupt handler for GPIO_EC_PACKET_MODE_DIS falling edge */ -void ec_comm_packet_mode_dis(enum gpio_signal unsed); -/* - * Return True if the given UART is in packet mode, in which EC-CR50 - * communication is on-going. - */ -int ec_comm_is_uart_in_packet_mode(int uart); -/* - * Try to process the given char as a EC-CR50 communication packet. - * If EC-CR50 communication is broken or uninitiated yet, then - * it does not process it. - * - * @return 1 if the given char was detected and processed as a part of packet. - * 0 otherwise. - */ -int ec_comm_process_packet(uint8_t ch); -/* - * Block or unblock EC-CR50 communication. - * @param block non-zero value blocks EC-CR50 communication. - * Zero value unblocks it. - */ -void ec_comm_block(int block); -/* Reset EC EFS context */ -void ec_efs_reset(void); -/* Set EC-EFS boot_mode */ -uint16_t ec_efs_set_boot_mode(const char *data, const uint8_t size); -/* Verify the given hash data against the EC-FW hash from kernel secdata */ -uint16_t ec_efs_verify_hash(const char *hash_data, const uint8_t size); - -/* Re-load EC Hash code from TPM Kernel Secdata */ -void ec_efs_refresh(void); -/* print EC-EFS status */ -void ec_efs_print_status(void); - -#endif /* __CROS_EC_COMM_H */ diff --git a/board/cr50/ec_efs.c b/board/cr50/ec_efs.c deleted file mode 100644 index 8f5bece989..0000000000 --- a/board/cr50/ec_efs.c +++ /dev/null @@ -1,257 +0,0 @@ -/* Copyright 2020 The Chromium OS Authors. All rights reserved. - * Use of this source code is governed by a BSD-style license that can be - * found in the LICENSE file. - * - * EC-EFS (Early Firmware Selection) - */ -#include "common.h" -#include "console.h" -#include "ec_comm.h" -#include "crc8.h" -#include "ec_commands.h" -#include "extension.h" -#include "hooks.h" -#include "registers.h" -#include "system.h" -#include "tpm_nvmem.h" -#include "tpm_nvmem_ops.h" -#include "vboot.h" - -#ifdef CR50_DEV -#define CPRINTS(format, args...) cprints(CC_TASK, "EC-EFS: " format, ## args) -#else -#define CPRINTS(format, args...) do { } while (0) -#endif - -/* - * Context of EC-EFS - */ -static struct ec_efs_context_ { - uint32_t boot_mode:8; /* enum ec_efs_boot_mode */ - uint32_t hash_is_loaded:1; /* Is EC hash loaded from nvmem */ - uint32_t reserved:23; - - uint32_t secdata_error_code; - - uint8_t hash[SHA256_DIGEST_SIZE]; /* EC-RW digest */ -} ec_efs_ctx; - -/* - * Change the boot mode - * - * @param mode_val New boot mode value to change - */ -static void set_boot_mode_(uint8_t mode_val) -{ - CPRINTS("boot_mode: 0x%02x -> 0x%02x", ec_efs_ctx.boot_mode, mode_val); - - ec_efs_ctx.boot_mode = mode_val; - - /* Backup some ec_efs context to scratch register */ - GREG32(PMU, PWRDN_SCRATCH20) &= ~0xff; - GREG32(PMU, PWRDN_SCRATCH20) |= mode_val; -} - -static int load_ec_hash_(uint8_t * const ec_hash) -{ - struct vb2_secdata_kernel secdata; - const uint8_t secdata_size = sizeof(struct vb2_secdata_kernel); - uint8_t size_to_crc; - uint8_t struct_size; - uint8_t crc; - - if (read_tpm_nvmem(KERNEL_NV_INDEX, secdata_size, - (void *)&secdata) != TPM_READ_SUCCESS) - return EC_ERROR_VBOOT_DATA_UNDERSIZED; - - /* - * Check struct version. CRC offset may be different with old struct - * version - */ - if (secdata.struct_version < VB2_SECDATA_KERNEL_STRUCT_VERSION_MIN) - return EC_ERROR_VBOOT_DATA_INCOMPATIBLE; - - /* Check struct size. */ - struct_size = secdata.struct_size; - if (struct_size != secdata_size) - return EC_ERROR_VBOOT_DATA; - - /* Check CRC */ - size_to_crc = struct_size - - offsetof(struct vb2_secdata_kernel, crc8) - - sizeof(secdata.crc8); - crc = crc8((uint8_t *)&secdata.reserved0, size_to_crc); - if (crc != secdata.crc8) - return EC_ERROR_CRC; - - /* Read hash and copy to hash */ - memcpy(ec_hash, secdata.ec_hash, sizeof(secdata.ec_hash)); - - return EC_SUCCESS; -} - -/* - * Initialize EC-EFS context. - */ -static void ec_efs_init_(void) -{ - if (!board_has_ec_cr50_comm_support()) - return; - - /* - * If it is a wakeup from deep sleep, then recover some core EC-EFS - * context values, including the boot_mode value, from a PWRD_SCRATCH - * register. Otherwise, reset boot_mode. - */ - if (system_get_reset_flags() & EC_RESET_FLAG_HIBERNATE) - set_boot_mode_(GREG32(PMU, PWRDN_SCRATCH20) & 0xff); - else - ec_efs_reset(); - - /* Read an EC hash in kernel secdata (TPM kernel NV index). */ - if (ec_efs_ctx.hash_is_loaded) - return; - - ec_efs_refresh(); -} -DECLARE_HOOK(HOOK_INIT, ec_efs_init_, HOOK_PRIO_DEFAULT); - -/** - * TPM vendor command handler to respond with EC Boot Mode. - * - * @return VENDOR_RC_SUCCESS - * - */ -static enum vendor_cmd_rc vc_get_boot_mode_(struct vendor_cmd_params *p) -{ - uint8_t *buffer; - - if (!board_has_ec_cr50_comm_support()) - return VENDOR_RC_NO_SUCH_SUBCOMMAND; - - buffer = (uint8_t *)p->buffer; - buffer[0] = (uint8_t)ec_efs_ctx.boot_mode; - - p->out_size = 1; - - return VENDOR_RC_SUCCESS; -} -DECLARE_VENDOR_COMMAND_P(VENDOR_CC_GET_BOOT_MODE, vc_get_boot_mode_); - -/** - * TPM vendor command handler to reset EC. - * - * @return VEDOR_RC_SUCCESS - */ -static enum vendor_cmd_rc vc_reset_ec_(struct vendor_cmd_params *p) -{ - if (!board_has_ec_cr50_comm_support()) - return VENDOR_RC_NO_SUCH_SUBCOMMAND; - - /* - * Let's reset EC a little later so that CR50 can send a TPM command - * to AP. - */ - board_reboot_ec_deferred(50 * MSEC); - - return VENDOR_RC_SUCCESS; -} -DECLARE_VENDOR_COMMAND_P(VENDOR_CC_RESET_EC, vc_reset_ec_); - -void ec_efs_reset(void) -{ - set_boot_mode_(EC_EFS_BOOT_MODE_NORMAL); -} - -/* - * Change the EC boot mode value. - * - * @param data Pointer to the EC-CR50 packet - * @param size Data (payload) size in EC-CR50 packet - * @return CR50_COMM_SUCCESS if the packet has been processed successfully, - * CR50_COMM_ERROR_SIZE if data size is not as expected, or - * 0 if it does not respond to EC. - */ -uint16_t ec_efs_set_boot_mode(const char * const data, const uint8_t size) -{ - uint8_t boot_mode; - - if (size != 1) - return CR50_COMM_ERROR_SIZE; - - boot_mode = data[0]; - - if (ec_efs_ctx.boot_mode != EC_EFS_BOOT_MODE_NORMAL) { - board_reboot_ec_deferred(0); - return 0; - } - - set_boot_mode_(boot_mode); - return CR50_COMM_SUCCESS; -} - -/* - * Verify the given EC-FW hash against one in kernel secdata. - * - * @param data Pointer to the EC-CR50 packet - * @param size Data (payload) size in EC-CR50 packet - * @return CR50_COMM_SUCCESS if the packet has been processed successfully, - * CR50_COMM_ERROR_SIZE if data size is not as expected, or - * CR50_COMM_ERROR_BAD_PAYLOAD if the given hash and the hash in NVM - * are not same. - * 0 if it deosn't have to respond to EC. - */ -uint16_t ec_efs_verify_hash(const char *hash_data, const uint8_t size) -{ - if (size != SHA256_DIGEST_SIZE) - return CR50_COMM_ERROR_SIZE; - - if (!ec_efs_ctx.hash_is_loaded) { - if (ec_efs_ctx.secdata_error_code == EC_SUCCESS) - ec_efs_refresh(); - - if (ec_efs_ctx.secdata_error_code != EC_SUCCESS) - return CR50_COMM_ERROR_NVMEM; - } - - if (safe_memcmp(hash_data, ec_efs_ctx.hash, SHA256_DIGEST_SIZE)) { - /* Verification failed */ - set_boot_mode_(EC_EFS_BOOT_MODE_NO_BOOT); - return CR50_COMM_ERROR_BAD_PAYLOAD; - } - - if (ec_efs_ctx.boot_mode != EC_EFS_BOOT_MODE_NORMAL) { - board_reboot_ec_deferred(0); - return 0; - } - - return CR50_COMM_SUCCESS; -} - -void ec_efs_refresh(void) -{ - int rv; - - rv = load_ec_hash_(ec_efs_ctx.hash); - if (rv == EC_SUCCESS) { - ec_efs_ctx.hash_is_loaded = 1; - } else { - ec_efs_ctx.hash_is_loaded = 0; - cprints(CC_SYSTEM, "load_ec_hash error: 0x%x\n", rv); - } - ec_efs_ctx.secdata_error_code = rv; -} - -void ec_efs_print_status(void) -{ - /* EC-EFS Context */ - ccprintf("ec_hash : %sLOADED\n", - ec_efs_ctx.hash_is_loaded ? "" : "UN"); - ccprintf("secdata_error_code : 0x%08x\n", - ec_efs_ctx.secdata_error_code); - -#ifdef CR50_RELAXED - ccprintf("ec_hash_secdata : %ph\n", - HEX_BUF(ec_efs_ctx.hash, SHA256_DIGEST_SIZE)); -#endif -} -- cgit v1.2.1