Merge changes I40c9ea47,I593aeed5
* changes: trusty: storage: add tests trusty: storage: add client lib for testing
This commit is contained in:
commit
c7878469be
5 changed files with 3571 additions and 0 deletions
37
trusty/storage/lib/Android.mk
Normal file
37
trusty/storage/lib/Android.mk
Normal file
|
@ -0,0 +1,37 @@
|
|||
#
|
||||
# Copyright (C) 2015 The Android Open-Source Project
|
||||
#
|
||||
# Licensed 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.
|
||||
#
|
||||
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := libtrustystorage
|
||||
|
||||
LOCAL_SRC_FILES := \
|
||||
storage.c \
|
||||
|
||||
LOCAL_CLFAGS = -fvisibility=hidden -Wall -Werror
|
||||
|
||||
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
|
||||
LOCAL_STATIC_LIBRARIES := \
|
||||
liblog \
|
||||
libtrusty \
|
||||
libtrustystorageinterface
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
154
trusty/storage/lib/include/trusty/lib/storage.h
Normal file
154
trusty/storage/lib/include/trusty/lib/storage.h
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <trusty/interface/storage.h>
|
||||
|
||||
#define STORAGE_MAX_NAME_LENGTH_BYTES 159
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
typedef uint32_t storage_session_t;
|
||||
typedef uint64_t file_handle_t;
|
||||
typedef uint64_t storage_off_t;
|
||||
|
||||
#define STORAGE_INVALID_SESSION ((storage_session_t)-1)
|
||||
|
||||
/**
|
||||
* storage_ops_flags - storage related operation flags
|
||||
* @STORAGE_OP_COMPLETE: forces to commit current transaction
|
||||
*/
|
||||
enum storage_ops_flags {
|
||||
STORAGE_OP_COMPLETE = 0x1,
|
||||
};
|
||||
|
||||
/**
|
||||
* storage_open_session() - Opens a storage session.
|
||||
* @device: device node for talking with Trusty
|
||||
* @session_p: pointer to location in which to store session handle
|
||||
* in case of success.
|
||||
*
|
||||
* Return: 0 on success, or an error code < 0 on failure.
|
||||
*/
|
||||
int storage_open_session(const char *device, storage_session_t *session_p, const char *port);
|
||||
|
||||
/**
|
||||
* storage_close_session() - Closes the session.
|
||||
* @session: the session to close
|
||||
*/
|
||||
void storage_close_session(storage_session_t session);
|
||||
|
||||
/**
|
||||
* storage_open_file() - Opens a file
|
||||
* @session: the storage_session_t returned from a call to storage_open_session
|
||||
* @handle_p: pointer to location in which to store file handle in case of success
|
||||
* @name: a null-terminated string identifier of the file to open.
|
||||
* Cannot be more than STORAGE_MAX_NAME_LENGTH_BYTES in length.
|
||||
* @flags: A bitmask consisting any storage_file_flag value or'ed together:
|
||||
* - STORAGE_FILE_OPEN_CREATE: if this file does not exist, create it.
|
||||
* - STORAGE_FILE_OPEN_CREATE_EXCLUSIVE: when specified, opening file with
|
||||
* STORAGE_OPEN_FILE_CREATE flag will
|
||||
* fail if the file already exists.
|
||||
* Only meaningful if used in combination
|
||||
* with STORAGE_FILE_OPEN_CREATE flag.
|
||||
* - STORAGE_FILE_OPEN_TRUNCATE: if this file already exists, discard existing
|
||||
* content and open it as a new file. No change
|
||||
* in semantics if the file does not exist.
|
||||
* @opflags: a combination of @storage_op_flags
|
||||
*
|
||||
* Return: 0 on success, or an error code < 0 on failure.
|
||||
*/
|
||||
int storage_open_file(storage_session_t session, file_handle_t *handle_p,
|
||||
const char *name, uint32_t flags, uint32_t opflags);
|
||||
|
||||
/**
|
||||
* storage_close_file() - Closes a file.
|
||||
* @handle: the file_handle_t retrieved from storage_open_file
|
||||
*/
|
||||
void storage_close_file(file_handle_t handle);
|
||||
|
||||
/**
|
||||
* storage_delete_file - Deletes a file.
|
||||
* @session: the storage_session_t returned from a call to storage_open_session
|
||||
* @name: the name of the file to delete
|
||||
* @opflags: a combination of @storage_op_flags
|
||||
*
|
||||
* Return: 0 on success, or an error code < 0 on failure.
|
||||
*/
|
||||
int storage_delete_file(storage_session_t session, const char *name,
|
||||
uint32_t opflags);
|
||||
|
||||
/**
|
||||
* storage_read() - Reads a file at a given offset.
|
||||
* @handle: the file_handle_t retrieved from storage_open_file
|
||||
* @off: the start offset from whence to read in the file
|
||||
* @buf: the buffer in which to write the data read
|
||||
* @size: the size of buf and number of bytes to read
|
||||
*
|
||||
* Return: the number of bytes read on success, negative error code on failure
|
||||
*/
|
||||
ssize_t storage_read(file_handle_t handle,
|
||||
storage_off_t off, void *buf, size_t size);
|
||||
|
||||
/**
|
||||
* storage_write() - Writes to a file at a given offset. Grows the file if necessary.
|
||||
* @handle: the file_handle_t retrieved from storage_open_file
|
||||
* @off: the start offset from whence to write in the file
|
||||
* @buf: the buffer containing the data to write
|
||||
* @size: the size of buf and number of bytes to write
|
||||
* @opflags: a combination of @storage_op_flags
|
||||
*
|
||||
* Return: the number of bytes written on success, negative error code on failure
|
||||
*/
|
||||
ssize_t storage_write(file_handle_t handle,
|
||||
storage_off_t off, const void *buf, size_t size,
|
||||
uint32_t opflags);
|
||||
|
||||
/**
|
||||
* storage_set_file_size() - Sets the size of the file.
|
||||
* @handle: the file_handle_t retrieved from storage_open_file
|
||||
* @off: the number of bytes to set as the new size of the file
|
||||
* @opflags: a combination of @storage_op_flags
|
||||
*
|
||||
* Return: 0 on success, negative error code on failure.
|
||||
*/
|
||||
int storage_set_file_size(file_handle_t handle, storage_off_t file_size,
|
||||
uint32_t opflags);
|
||||
|
||||
/**
|
||||
* storage_get_file_size() - Gets the size of the file.
|
||||
* @session: the storage_session_t returned from a call to storage_open_session
|
||||
* @handle: the file_handle_t retrieved from storage_open_file
|
||||
* @size: pointer to storage_off_t in which to store the file size
|
||||
*
|
||||
* Return: 0 on success, negative error code on failure.
|
||||
*/
|
||||
int storage_get_file_size(file_handle_t handle, storage_off_t *size);
|
||||
|
||||
|
||||
/**
|
||||
* storage_end_transaction: End current transaction
|
||||
* @session: the storage_session_t returned from a call to storage_open_session
|
||||
* @complete: if true, commit current transaction, discard it otherwise
|
||||
*
|
||||
* Return: 0 on success, negative error code on failure.
|
||||
*/
|
||||
int storage_end_transaction(storage_session_t session, bool complete);
|
||||
|
||||
|
||||
__END_DECLS
|
311
trusty/storage/lib/storage.c
Normal file
311
trusty/storage/lib/storage.c
Normal file
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include <trusty/tipc.h>
|
||||
#include <trusty/lib/storage.h>
|
||||
|
||||
#define LOG_TAG "trusty_storage_client"
|
||||
#include <cutils/log.h>
|
||||
|
||||
#define MAX_CHUNK_SIZE 4040
|
||||
|
||||
static inline file_handle_t make_file_handle(storage_session_t s, uint32_t fid)
|
||||
{
|
||||
return ((uint64_t)s << 32) | fid;
|
||||
}
|
||||
|
||||
static inline storage_session_t _to_session(file_handle_t fh)
|
||||
{
|
||||
return (storage_session_t)(fh >> 32);
|
||||
}
|
||||
|
||||
static inline uint32_t _to_handle(file_handle_t fh)
|
||||
{
|
||||
return (uint32_t) fh;
|
||||
}
|
||||
|
||||
static inline uint32_t _to_msg_flags(uint32_t opflags)
|
||||
{
|
||||
uint32_t msg_flags = 0;
|
||||
|
||||
if (opflags & STORAGE_OP_COMPLETE)
|
||||
msg_flags |= STORAGE_MSG_FLAG_TRANSACT_COMPLETE;
|
||||
|
||||
return msg_flags;
|
||||
}
|
||||
|
||||
static ssize_t check_response(struct storage_msg *msg, ssize_t res)
|
||||
{
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
if ((size_t)res < sizeof(*msg)) {
|
||||
ALOGE("invalid msg length (%zd < %zd)\n", res, sizeof(*msg));
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
ALOGV("cmd 0x%x: server returned %u\n", msg->cmd, msg->result);
|
||||
|
||||
switch(msg->result) {
|
||||
case STORAGE_NO_ERROR:
|
||||
return res - sizeof(*msg);
|
||||
|
||||
case STORAGE_ERR_NOT_FOUND:
|
||||
return -ENOENT;
|
||||
|
||||
case STORAGE_ERR_EXIST:
|
||||
return -EEXIST;
|
||||
|
||||
case STORAGE_ERR_NOT_VALID:
|
||||
return -EINVAL;
|
||||
|
||||
case STORAGE_ERR_UNIMPLEMENTED:
|
||||
ALOGE("cmd 0x%x: is unhandles command\n", msg->cmd);
|
||||
return -EINVAL;
|
||||
|
||||
case STORAGE_ERR_ACCESS:
|
||||
return -EACCES;
|
||||
|
||||
case STORAGE_ERR_TRANSACT:
|
||||
return -EBUSY;
|
||||
|
||||
case STORAGE_ERR_GENERIC:
|
||||
ALOGE("cmd 0x%x: internal server error\n", msg->cmd);
|
||||
return -EIO;
|
||||
|
||||
default:
|
||||
ALOGE("cmd 0x%x: unhandled server response %u\n",
|
||||
msg->cmd, msg->result);
|
||||
}
|
||||
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
static ssize_t send_reqv(storage_session_t session,
|
||||
const struct iovec *tx_iovs, uint tx_iovcnt,
|
||||
const struct iovec *rx_iovs, uint rx_iovcnt)
|
||||
{
|
||||
ssize_t rc;
|
||||
|
||||
rc = writev(session, tx_iovs, tx_iovcnt);
|
||||
if (rc < 0) {
|
||||
rc = -errno;
|
||||
ALOGE("failed to send request: %s\n", strerror(errno));
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = readv(session, rx_iovs, rx_iovcnt);
|
||||
if (rc < 0) {
|
||||
rc = -errno;
|
||||
ALOGE("failed to recv response: %s\n", strerror(errno));
|
||||
return rc;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
int storage_open_session(const char *device, storage_session_t *session_p,
|
||||
const char *port)
|
||||
{
|
||||
int rc = tipc_connect(device, port);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
*session_p = (storage_session_t) rc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void storage_close_session(storage_session_t session)
|
||||
{
|
||||
tipc_close(session);
|
||||
}
|
||||
|
||||
|
||||
int storage_open_file(storage_session_t session, file_handle_t *handle_p, const char *name,
|
||||
uint32_t flags, uint32_t opflags)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_OPEN, .flags = _to_msg_flags(opflags)};
|
||||
struct storage_file_open_req req = { .flags = flags };
|
||||
struct iovec tx[3] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}, {(void *)name, strlen(name)}};
|
||||
struct storage_file_open_resp rsp = { 0 };
|
||||
struct iovec rx[2] = {{&msg, sizeof(msg)}, {&rsp, sizeof(rsp)}};
|
||||
|
||||
ssize_t rc = send_reqv(session, tx, 3, rx, 2);
|
||||
rc = check_response(&msg, rc);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
|
||||
if ((size_t)rc != sizeof(rsp)) {
|
||||
ALOGE("%s: invalid response length (%zd != %zd)\n", __func__, rc, sizeof(rsp));
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
*handle_p = make_file_handle(session, rsp.handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void storage_close_file(file_handle_t fh)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_CLOSE };
|
||||
struct storage_file_close_req req = { .handle = _to_handle(fh)};
|
||||
struct iovec tx[2] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}};
|
||||
struct iovec rx[1] = {{&msg, sizeof(msg)}};
|
||||
|
||||
ssize_t rc = send_reqv(_to_session(fh), tx, 2, rx, 1);
|
||||
rc = check_response(&msg, rc);
|
||||
if (rc < 0) {
|
||||
ALOGE("close file failed (%d)\n", (int)rc);
|
||||
}
|
||||
}
|
||||
|
||||
int storage_delete_file(storage_session_t session, const char *name, uint32_t opflags)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_DELETE, .flags = _to_msg_flags(opflags)};
|
||||
struct storage_file_delete_req req = { .flags = 0, };
|
||||
struct iovec tx[3] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}, {(void *)name, strlen(name)}};
|
||||
struct iovec rx[1] = {{&msg, sizeof(msg)}};
|
||||
|
||||
ssize_t rc = send_reqv(session, tx, 3, rx, 1);
|
||||
return check_response(&msg, rc);
|
||||
}
|
||||
|
||||
static int _read_chunk(file_handle_t fh, storage_off_t off, void *buf, size_t size)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_READ };
|
||||
struct storage_file_read_req req = { .handle = _to_handle(fh), .size = size, .offset = off };
|
||||
struct iovec tx[2] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}};
|
||||
struct iovec rx[2] = {{&msg, sizeof(msg)}, {buf, size}};
|
||||
|
||||
ssize_t rc = send_reqv(_to_session(fh), tx, 2, rx, 2);
|
||||
return check_response(&msg, rc);
|
||||
}
|
||||
|
||||
ssize_t storage_read(file_handle_t fh, storage_off_t off, void *buf, size_t size)
|
||||
{
|
||||
int rc;
|
||||
size_t bytes_read = 0;
|
||||
size_t chunk = MAX_CHUNK_SIZE;
|
||||
uint8_t *ptr = buf;
|
||||
|
||||
while (size) {
|
||||
if (chunk > size)
|
||||
chunk = size;
|
||||
rc = _read_chunk(fh, off, ptr, chunk);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
if (rc == 0)
|
||||
break;
|
||||
off += rc;
|
||||
ptr += rc;
|
||||
bytes_read += rc;
|
||||
size -= rc;
|
||||
}
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
static int _write_req(file_handle_t fh, storage_off_t off,
|
||||
const void *buf, size_t size, uint32_t msg_flags)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_WRITE, .flags = msg_flags, };
|
||||
struct storage_file_write_req req = { .handle = _to_handle(fh), .offset = off, };
|
||||
struct iovec tx[3] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}, {(void *)buf, size}};
|
||||
struct iovec rx[1] = {{&msg, sizeof(msg)}};
|
||||
|
||||
ssize_t rc = send_reqv(_to_session(fh), tx, 3, rx, 1);
|
||||
rc = check_response(&msg, rc);
|
||||
return rc < 0 ? rc : size;
|
||||
}
|
||||
|
||||
ssize_t storage_write(file_handle_t fh, storage_off_t off,
|
||||
const void *buf, size_t size, uint32_t opflags)
|
||||
{
|
||||
int rc;
|
||||
size_t bytes_written = 0;
|
||||
size_t chunk = MAX_CHUNK_SIZE;
|
||||
const uint8_t *ptr = buf;
|
||||
uint32_t msg_flags = _to_msg_flags(opflags & ~STORAGE_OP_COMPLETE);
|
||||
|
||||
while (size) {
|
||||
if (chunk >= size) {
|
||||
/* last chunk in sequence */
|
||||
chunk = size;
|
||||
msg_flags = _to_msg_flags(opflags);
|
||||
}
|
||||
rc = _write_req(fh, off, ptr, chunk, msg_flags);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
if ((size_t)rc != chunk) {
|
||||
ALOGE("got partial write (%d)\n", (int)rc);
|
||||
return -EIO;
|
||||
}
|
||||
off += chunk;
|
||||
ptr += chunk;
|
||||
bytes_written += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
int storage_set_file_size(file_handle_t fh, storage_off_t file_size, uint32_t opflags)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_SET_SIZE, .flags = _to_msg_flags(opflags)};
|
||||
struct storage_file_set_size_req req = { .handle = _to_handle(fh), .size = file_size, };
|
||||
struct iovec tx[2] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}};
|
||||
struct iovec rx[1] = {{&msg, sizeof(msg)}};
|
||||
|
||||
ssize_t rc = send_reqv(_to_session(fh), tx, 2, rx, 1);
|
||||
return check_response(&msg, rc);
|
||||
}
|
||||
|
||||
int storage_get_file_size(file_handle_t fh, storage_off_t *size_p)
|
||||
{
|
||||
struct storage_msg msg = { .cmd = STORAGE_FILE_GET_SIZE };
|
||||
struct storage_file_get_size_req req = { .handle = _to_handle(fh), };
|
||||
struct iovec tx[2] = {{&msg, sizeof(msg)}, {&req, sizeof(req)}};
|
||||
struct storage_file_get_size_resp rsp;
|
||||
struct iovec rx[2] = {{&msg, sizeof(msg)}, {&rsp, sizeof(rsp)}};
|
||||
|
||||
ssize_t rc = send_reqv(_to_session(fh), tx, 2, rx, 2);
|
||||
rc = check_response(&msg, rc);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
|
||||
if ((size_t)rc != sizeof(rsp)) {
|
||||
ALOGE("%s: invalid response length (%zd != %zd)\n", __func__, rc, sizeof(rsp));
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
*size_p = rsp.size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int storage_end_transaction(storage_session_t session, bool complete)
|
||||
{
|
||||
struct storage_msg msg = {
|
||||
.cmd = STORAGE_END_TRANSACTION,
|
||||
.flags = complete ? STORAGE_MSG_FLAG_TRANSACT_COMPLETE : 0,
|
||||
};
|
||||
struct iovec iov = {&msg, sizeof(msg)};
|
||||
|
||||
ssize_t rc = send_reqv(session, &iov, 1, &iov, 1);
|
||||
return check_response(&msg, rc);
|
||||
}
|
29
trusty/storage/tests/Android.mk
Normal file
29
trusty/storage/tests/Android.mk
Normal file
|
@ -0,0 +1,29 @@
|
|||
#
|
||||
# Copyright (C) 2016 The Android Open Source Project
|
||||
#
|
||||
# Licensed 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.
|
||||
#
|
||||
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := secure-storage-unit-test
|
||||
LOCAL_CFLAGS += -g -Wall -Werror -std=gnu++11 -Wno-missing-field-initializers
|
||||
LOCAL_STATIC_LIBRARIES := \
|
||||
libtrustystorageinterface \
|
||||
libtrustystorage \
|
||||
libtrusty \
|
||||
liblog
|
||||
LOCAL_SRC_FILES := main.cpp
|
||||
include $(BUILD_NATIVE_TEST)
|
||||
|
3040
trusty/storage/tests/main.cpp
Normal file
3040
trusty/storage/tests/main.cpp
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue