├── .gitignore ├── .gitmodules ├── .clang_complete ├── src ├── mc_writer.h ├── utils.h ├── ecc.h ├── ecc.c ├── ps2mcfs.h ├── fat.h ├── mkfs_ps2.c ├── tests.c ├── vmc_types.h ├── mc_writer.c ├── fat.c ├── fuseps2mc.c └── ps2mcfs.c ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── Makefile ├── .clang-format └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.swp 3 | bin/* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/munit"] 2 | path = vendor/munit 3 | url = https://github.com/nemequ/munit.git 4 | -------------------------------------------------------------------------------- /.clang_complete: -------------------------------------------------------------------------------- 1 | -I/usr/include/fuse3 2 | 3 | -I./vendor 4 | -Wall 5 | -ggdb3 6 | -O0 7 | -std=gnu11 8 | -D 9 | DEBUG=1 10 | -------------------------------------------------------------------------------- /src/mc_writer.h: -------------------------------------------------------------------------------- 1 | #ifndef __MC_WRITER_H__ 2 | #define __MC_WRITER_H__ 3 | 4 | #include 5 | #include "vmc_types.h" 6 | 7 | 8 | static const unsigned PAGE_SPARE_PART_SIZE = 16; 9 | 10 | /**Writes an empty memory card file with the geometry described by the given superblock*/ 11 | int mc_writer_write_empty(const superblock_t* superblock, FILE* output_file); 12 | 13 | #endif -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef _UTILS_H_ 2 | #define _UTILS_H_ 3 | 4 | #define div_ceil(x, y) ((x)/(y) + ((x)%(y) != 0)) 5 | 6 | #define MIN(a,b) ((a)<(b) ? (a) : (b)) 7 | #define MAX(a,b) ((a)<(b) ? (b) : (a)) 8 | 9 | #ifdef DEBUG 10 | #define DEBUG_printf(...) printf(__VA_ARGS__) 11 | #else 12 | #define DEBUG_printf(...) 13 | #endif 14 | 15 | #define SWAP(x, y) { typeof(x) SWAP = x; x = y; y = SWAP; } 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | paths: 5 | - Makefile 6 | - src/* 7 | - '.github/**' 8 | 9 | jobs: 10 | build_and_test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up git repository 14 | uses: actions/checkout@v3 15 | with: 16 | submodules: 'true' 17 | 18 | - name: Install dependencies 19 | run: sudo apt-get install -y libfuse3-dev pkgconf clang-tools 20 | 21 | - name: Compile 22 | run: make 23 | 24 | - name: Run tests 25 | run: bin/tests 26 | -------------------------------------------------------------------------------- /src/ecc.h: -------------------------------------------------------------------------------- 1 | #ifndef __ECC_H__ 2 | #define __ECC_H__ 3 | 4 | #include 5 | #include 6 | 7 | // data size is always 128 byte (i.e: page chunk size) 8 | 9 | // reads the 128 bytes of data pointed at by `data_src` and writes the 3-byte hamming code in `ecc_dest` 10 | void ecc128_calculate(uint8_t* ecc_dest, uint8_t* data_src); 11 | 12 | // verifies the 3-byte hamming code pointed at by `ecc_src` against the 128 bytes of data pointed at by `data_src` 13 | bool ecc128_check(uint8_t* ecc_src, uint8_t* data_src); 14 | 15 | // calculates the ecc bytes for a whole page 16 | void ecc512_calculate(uint8_t* ecc_dest, uint8_t* data_src); 17 | 18 | // verifies the ecc bytes for a whole page 19 | bool ecc512_check(uint8_t* ecc_src, uint8_t* data_src); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | build_and_upload: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set up git repository 12 | uses: actions/checkout@v3 13 | with: 14 | submodules: 'true' 15 | 16 | - name: Install dependencies 17 | run: sudo apt-get install -y libfuse3-dev pkgconf clang-tools 18 | 19 | - name: Compile 20 | run: make 21 | 22 | - name: Upload 23 | run: | 24 | upload_url=$( 25 | curl -L https://api.github.com/repos/$GITHUB_REPOSITORY/releases/tags/${{ github.ref_name }} \ 26 | -H "Accept: application/vnd.github+json" \ 27 | -H "Authorization: Bearer ${{ secrets.REPO_ACCESS_TOKEN }}" \ 28 | -H "X-GitHub-Api-Version: 2022-11-28" | jq -r .upload_url 29 | ) 30 | # remove suffix from returned url 31 | upload_url=${upload_url/{*/} 32 | 33 | zipname=ps2mcfs-${{ github.ref_name }}-linux-amd64.zip 34 | zip --exclude bin/tests -r -j "$zipname" bin/ 35 | 36 | curl -L ${upload_url}?name=$(echo "$zipname" | sed -e 's/%/%25/g' -e 's/\+/%2B/g' -e 's/ /%20/g') \ 37 | -X POST \ 38 | -H "Accept: application/vnd.github+json" \ 39 | -H "Authorization: Bearer ${{ secrets.REPO_ACCESS_TOKEN }}"\ 40 | -H "Content-Type: application/octet-stream" \ 41 | -H "X-GitHub-Api-Version: 2022-11-28" \ 42 | --data-binary "@$zipname" 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | BIN_DIR = bin 3 | OBJ_DIR = obj 4 | INC_DIR = src 5 | SRC_DIR = src 6 | 7 | OBJS = $(addprefix $(OBJ_DIR)/, ps2mcfs.o fat.o ecc.o mc_writer.o) 8 | INCLUDES = $(addprefix $(INC_DIR)/, ps2mcfs.h fat.h ecc.h vmc_types.h utils.h) 9 | 10 | TEST_OBJS = $(addprefix $(OBJ_DIR)/, munit.o) # test-only objects 11 | TEST_INCLUDES = vendor/munit/munit.h # test-only includes 12 | 13 | CC = cc 14 | CFLAGS = $(shell pkg-config fuse3 --cflags) -I./vendor -Wall -ggdb3 -O0 -std=gnu11 -D DEBUG=1 15 | LIBS = $(shell pkg-config fuse3 --libs) 16 | 17 | .PHONY: clean all 18 | 19 | all: .clang_complete $(BIN_DIR)/fuseps2mc $(BIN_DIR)/mkfs.ps2 $(BIN_DIR)/tests 20 | 21 | $(OBJ_DIR)/munit.o: vendor/munit/munit.c vendor/munit/munit.h 22 | mkdir -p $(OBJ_DIR) 23 | $(CC) $(CFLAGS) -c "$<" -o "$@" 24 | 25 | $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(INCLUDES) Makefile 26 | mkdir -p $(OBJ_DIR) 27 | $(CC) $(CFLAGS) -c "$<" -o "$@" 28 | 29 | .clang_complete: Makefile 30 | echo "$(CFLAGS)" | tr " " "\n" > $@ 31 | 32 | clean: 33 | rm -f $(OBJS) 34 | 35 | # vendor dependencies 36 | vendor/munit/%: 37 | git submodule update -f -- vendor/munit/ 38 | 39 | # executables 40 | 41 | $(BIN_DIR)/fuseps2mc: $(OBJ_DIR)/fuseps2mc.o $(OBJS) $(INCLUDES) Makefile 42 | mkdir -p $(BIN_DIR) 43 | $(CC) $< $(OBJS) $(CFLAGS) $(LIBS) -o "$@" 44 | 45 | $(BIN_DIR)/mkfs.ps2: $(OBJ_DIR)/mkfs_ps2.o $(OBJS) $(INCLUDES) Makefile 46 | mkdir -p $(BIN_DIR) 47 | $(CC) $< $(OBJS) $(CFLAGS) $(LIBS) -o "$@" 48 | 49 | $(BIN_DIR)/tests: $(OBJ_DIR)/tests.o $(OBJS) $(TEST_OBJS) $(INCLUDES) $(TEST_INCLUDES) 50 | mkdir -p $(BIN_DIR) 51 | $(CC) $< $(OBJS) $(TEST_OBJS) $(CFLAGS) $(LIBS) -o "$@" 52 | -------------------------------------------------------------------------------- /src/ecc.c: -------------------------------------------------------------------------------- 1 | /** 2 | * This hamming code implementation was heavily based on the implementationn in the `mymc` project by Ross Ridge 3 | * A copy of the project maintained by the ps2sdk devs can be found at https://github.com/ps2dev/mymc 4 | */ 5 | 6 | #include 7 | #include 8 | #include "ecc.h" 9 | 10 | // returns 0 if an even number of bits is set. returns 1 of an odd number of bits is set 11 | bool ecc_byte_parity(uint8_t x) { 12 | x ^= x >> 1; 13 | x ^= x >> 2; 14 | x ^= x >> 4; 15 | return x & 1; 16 | } 17 | 18 | uint8_t ecc_column_parity_mask(uint8_t x) { 19 | return ecc_byte_parity(x & 0x55) << 0 // 0b01010101 20 | | ecc_byte_parity(x & 0x33) << 1 // 0b00110011 21 | | ecc_byte_parity(x & 0x0F) << 2 // 0b00001111 22 | | ecc_byte_parity(x & 0x00) << 3 // 0b00000000 23 | | ecc_byte_parity(x & 0xAA) << 4 // 0b10101010 24 | | ecc_byte_parity(x & 0xCC) << 5 // 0b11001100 25 | | ecc_byte_parity(x & 0xF0) << 6; // 0b11110000 26 | } 27 | 28 | 29 | 30 | void ecc128_calculate(uint8_t* ecc_dest, uint8_t* data_src) { 31 | uint8_t* column_parity = ecc_dest; 32 | uint8_t* line_parity_0 = ecc_dest+1; 33 | uint8_t* line_parity_1 = ecc_dest+2; 34 | 35 | *column_parity = 0x77; // 0b01110111 36 | *line_parity_0 = 0x7F; // 0b01111111 37 | *line_parity_1 = 0x7F; // 0b01111111 38 | 39 | for (unsigned i = 0; i < 128; ++i) { 40 | *column_parity ^= ecc_column_parity_mask(data_src[i]); 41 | if (ecc_byte_parity(data_src[i]) != 0) { // if is odd 42 | *line_parity_0 ^= ~i; 43 | *line_parity_1 ^= i; 44 | } 45 | } 46 | *line_parity_0 &= 0x7F; 47 | } 48 | 49 | bool ecc128_check(uint8_t* ecc_src, uint8_t* data_src) { 50 | // TODO: perform actual verification and error correction 51 | uint8_t calculated[3]; 52 | ecc128_calculate(calculated, data_src); 53 | return calculated[0] == ecc_src[0] 54 | && calculated[1] == ecc_src[1] 55 | && calculated[2] == ecc_src[2]; 56 | } 57 | 58 | 59 | void ecc512_calculate(uint8_t* ecc_dest, uint8_t* data_src) { 60 | for (unsigned i = 0; i < 512/128; ++i) { 61 | ecc128_calculate(ecc_dest + i * 3, data_src + i * 128); 62 | } 63 | } 64 | 65 | bool ecc512_check(uint8_t* ecc_src, uint8_t* data_src) { 66 | for (unsigned i = 0; i < 512/128; ++i) { 67 | if (!ecc128_check(ecc_src + i * 3, data_src + i * 128)) 68 | return false; 69 | } 70 | return true; 71 | } -------------------------------------------------------------------------------- /src/ps2mcfs.h: -------------------------------------------------------------------------------- 1 | #ifndef __PS2MCFS_H__ 2 | #define __PS2MCFS_H__ 3 | 4 | #include 5 | #include 6 | #include // struct stat 7 | 8 | #include "fat.h" 9 | 10 | /** 11 | * Converts a UNIX `time_t` struct into an equivalent `date_time_t` struct that is suitable for storing a PS2 FAT direntry 12 | */ 13 | void ps2mcfs_time_to_date_time(time_t thetime, date_time_t* dt); 14 | 15 | bool ps2mcfs_is_directory(const dir_entry_t* const dirent); 16 | bool ps2mcfs_is_file(const dir_entry_t* const dirent); 17 | 18 | /** 19 | * Reads the superblock from the vmc data and initializes the field in the output metadata struct 20 | */ 21 | int ps2mcfs_get_superblock(struct vmc_meta* metadata_out); 22 | 23 | void ps2mcfs_ls(const struct vmc_meta* vmc_meta, dir_entry_t* parent, int(* cb)(dir_entry_t* child, void* extra), void* extra); 24 | int ps2mcfs_browse(const struct vmc_meta* vmc_meta, dir_entry_t* root, const char* path, browse_result_t* dest); 25 | dir_entry_t ps2mcfs_locate(const struct vmc_meta* vmc_meta, browse_result_t* src); 26 | 27 | void ps2mcfs_stat(const dir_entry_t* const dirent, struct stat* stbuf); 28 | int ps2mcfs_read(const struct vmc_meta* vmc_meta, const dir_entry_t* dirent, void* buf, size_t size, off_t offset); 29 | 30 | void ps2mcfs_utime(const struct vmc_meta* vmc_meta, browse_result_t* dirent, date_time_t modification); 31 | 32 | /** 33 | * Creates a directory entry for a new subdirectory 34 | */ 35 | int ps2mcfs_mkdir(const struct vmc_meta* vmc_meta, dir_entry_t* parent, const char* name, uint16_t mode); 36 | 37 | /** 38 | * Creates a directory entry for a new file 39 | */ 40 | int ps2mcfs_create(const struct vmc_meta* vmc_meta, dir_entry_t* parent, const char* name, cluster_t cluster, uint16_t mode); 41 | 42 | /** 43 | * Writes data into the file referenced by the directory entry 44 | */ 45 | int ps2mcfs_write(const struct vmc_meta* vmc_meta, const browse_result_t* dirent, const void* buf, size_t size, off_t offset); 46 | 47 | 48 | int ps2mcfs_unlink(const struct vmc_meta* vmc_meta, const dir_entry_t unlinked_file, const dir_entry_t parent, size_t index_in_parent); 49 | 50 | int ps2mcfs_rmdir(const struct vmc_meta* vmc_meta, const dir_entry_t removed_dir, const dir_entry_t parent, size_t index_in_parent); 51 | 52 | int ps2mcfs_get_child(const struct vmc_meta* vmc_meta, cluster_t clus0, unsigned int entrynum, dir_entry_t* dest); 53 | int ps2mcfs_set_child(const struct vmc_meta* vmc_meta, cluster_t clus0, unsigned int entrynum, dir_entry_t* src); 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/fat.h: -------------------------------------------------------------------------------- 1 | #ifndef __FAT_H__ 2 | #define __FAT_H__ 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "vmc_types.h" 10 | 11 | 12 | /** 13 | * Returns the physical size of a cluster in bytes (including ECC bytes) 14 | */ 15 | size_t fat_cluster_size(const struct vmc_meta* vmc_meta); 16 | 17 | /** 18 | * Returns the logical size of a cluster in bytes 19 | */ 20 | size_t fat_cluster_capacity(const struct vmc_meta* vmc_meta); 21 | 22 | /** 23 | * Returns the phyisical byte offset for the given allocatable cluster index and a 24 | * byte offset relative to the start of the memory card 25 | */ 26 | physical_offset_t fat_logical_to_physical_offset(const struct vmc_meta* vmc_meta, cluster_t cluster, logical_offset_t bytes_offset); 27 | 28 | /** 29 | * Returns the FAT table entry value for a cluster 30 | */ 31 | union fat_entry fat_get_table_entry(const struct vmc_meta* vmc_meta, cluster_t clus); 32 | 33 | /** 34 | * Sets the FAT table entry for a cluster 35 | */ 36 | void fat_set_table_entry(const struct vmc_meta* vmc_meta, cluster_t clus, union fat_entry newval); 37 | 38 | /** 39 | * Move forward `count` clusters in the chain starting from clus0. 40 | * Returns CLUSTER_INVALID if tried to move past the end of the chain. 41 | **/ 42 | cluster_t fat_seek(const struct vmc_meta* vmc_meta, cluster_t clus0, size_t count); 43 | 44 | /** 45 | * Returns a free cluster or 0xFFFFFFFF if none is found. 46 | * 'clus' should be the start cluster for the search. 47 | **/ 48 | cluster_t fat_find_free_cluster(const struct vmc_meta* vmc_meta, cluster_t clus); 49 | 50 | /** 51 | * Makes the linked list that starts at `clus` span `count` clusters. Frees old clusters or allocates new ones if needed. 52 | * Returns the last cluster or CLUSTER_INVALID if ran out of space while extending the list. 53 | * NOTE: the cluster chain should not start at CLUSTER_INVALID to avoid a segmentation fault 54 | **/ 55 | cluster_t fat_truncate(const struct vmc_meta* vmc_meta, cluster_t clus, size_t count); 56 | 57 | /** 58 | * Creates a new list spanning 'len' clusters 59 | * returns the first cluster or 0xFFFFFFFF if not enough space 60 | **/ 61 | cluster_t fat_allocate(const struct vmc_meta* vmc_meta, size_t len); 62 | 63 | size_t fat_read_bytes(const struct vmc_meta* vmc_meta, cluster_t clus0, logical_offset_t offset, size_t size, void* buf); 64 | size_t fat_write_bytes(const struct vmc_meta* vmc_meta, cluster_t clus0, logical_offset_t offset, size_t size, const void* buf); 65 | 66 | #endif -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: WebKit 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: DontAlign 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlinesLeft: false 9 | AlignOperands: false 10 | AlignTrailingComments: false 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: true 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: false 21 | BinPackArguments: true 22 | BinPackParameters: true 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | BeforeCatch: false 33 | BeforeElse: true 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: All 36 | BreakBeforeBraces: Custom 37 | BreakBeforeTernaryOperators: true 38 | BreakConstructorInitializersBeforeComma: true 39 | BreakAfterJavaFieldAnnotations: false 40 | BreakStringLiterals: true 41 | ColumnLimit: 0 42 | CommentPragmas: '^ IWYU pragma:' 43 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 44 | ConstructorInitializerIndentWidth: 4 45 | ContinuationIndentWidth: 4 46 | Cpp11BracedListStyle: false 47 | DerivePointerAlignment: false 48 | DisableFormat: false 49 | ExperimentalAutoDetectBinPacking: false 50 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 51 | IncludeCategories: 52 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 53 | Priority: 2 54 | - Regex: '^(<|"(gtest|isl|json)/)' 55 | Priority: 3 56 | - Regex: '.*' 57 | Priority: 1 58 | IncludeIsMainRegex: '$' 59 | IndentCaseLabels: false 60 | IndentWidth: 4 61 | IndentWrappedFunctionNames: false 62 | JavaScriptQuotes: Leave 63 | JavaScriptWrapImports: true 64 | KeepEmptyLinesAtTheStartOfBlocks: true 65 | MacroBlockBegin: '' 66 | MacroBlockEnd: '' 67 | MaxEmptyLinesToKeep: 1 68 | NamespaceIndentation: Inner 69 | ObjCBlockIndentWidth: 4 70 | ObjCSpaceAfterProperty: true 71 | ObjCSpaceBeforeProtocolList: true 72 | PenaltyBreakBeforeFirstCallParameter: 19 73 | PenaltyBreakComment: 300 74 | PenaltyBreakFirstLessLess: 120 75 | PenaltyBreakString: 1000 76 | PenaltyExcessCharacter: 1000000 77 | PenaltyReturnTypeOnItsOwnLine: 60 78 | PointerAlignment: Left 79 | ReflowComments: true 80 | SortIncludes: true 81 | SpaceAfterCStyleCast: true 82 | SpaceBeforeAssignmentOperators: true 83 | SpaceBeforeParens: ControlStatements 84 | SpaceInEmptyParentheses: false 85 | SpacesBeforeTrailingComments: 1 86 | SpacesInAngles: false 87 | SpacesInContainerLiterals: false 88 | SpacesInCStyleCastParentheses: false 89 | SpacesInParentheses: false 90 | SpacesInSquareBrackets: false 91 | Standard: Cpp11 92 | TabWidth: 4 93 | UseTab: Always 94 | ... 95 | 96 | -------------------------------------------------------------------------------- /src/mkfs_ps2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "vmc_types.h" 7 | #include "mc_writer.h" 8 | 9 | 10 | static const struct option CLI_OPTIONS[] = { 11 | {.name = "size", .has_arg = required_argument, .flag = NULL, .val = 0}, 12 | {.name = "ecc", .has_arg = no_argument, .flag = NULL, .val = 0}, 13 | {.name = "output", .has_arg = required_argument, .flag = NULL, .val = 0}, 14 | {.name = "help", .has_arg = no_argument, .flag = NULL, .val = 0}, 15 | {.name = NULL, .has_arg = 0, .flag = NULL, .val = 0} 16 | }; 17 | 18 | void usage(FILE* stream, const char* program_name, int exit_code) { 19 | fprintf( 20 | stream, 21 | "Usage: %s -o OUTPUT_FILE [-s SIZE] [-e] [-h]\n" 22 | "Create a virtual memory card image file.\n" 23 | "\n" 24 | " -s, --size=NUM \tSet the memory card size in megabytes (options: 8)\n" 25 | " -e, --ecc \tAdd ECC bytes to the generated file\n" 26 | " -o, --output=FILE\tSet the output file\n" 27 | " -h, --help \tShow this help\n", 28 | program_name 29 | ); 30 | exit(exit_code); 31 | } 32 | 33 | int copy_optarg(char** dest) { 34 | if (!optarg) 35 | return 0; 36 | size_t optarg_len = strlen(optarg); 37 | *dest = malloc(optarg_len); 38 | strcpy(*dest, optarg); 39 | return optarg_len; 40 | } 41 | 42 | int main(int argc, char** argv) { 43 | // initialize default superblock 44 | superblock_t superblock; 45 | memcpy(&superblock, &DEFAULT_SUPERBLOCK, sizeof(superblock_t)); 46 | 47 | // parse options 48 | char* option_output_filename = NULL; 49 | int opt; 50 | int long_option_index = 0; 51 | while ((opt = getopt_long(argc, argv, "s:eo:h", CLI_OPTIONS, &long_option_index)) != -1) { 52 | // parse -s / --size option 53 | if ((opt == 0 && long_option_index == 0) || opt == 's') { 54 | if (strcmp(optarg, "8") == 0) { 55 | superblock.clusters_per_card = 8192; 56 | } 57 | else { 58 | fprintf(stderr, "Invalid SIZE value: %s. Allowed values: 8.\n", optarg); 59 | usage(stderr, argv[0], EXIT_FAILURE); 60 | } 61 | } 62 | // parse -e / --ecc option 63 | else if ((opt == 0 && long_option_index == 1) || opt == 'e') { 64 | superblock.card_flags |= CF_USE_ECC; 65 | } 66 | // parse -o / --output option 67 | else if ((opt == 0 && long_option_index == 2) || opt == 'o') { 68 | if (!copy_optarg(&option_output_filename)) { 69 | fprintf(stderr, "Invalid value: %s.\n", argv[optind]); 70 | usage(stderr, argv[0], EXIT_FAILURE); 71 | } 72 | } 73 | // parse -h / --help option 74 | else if ((opt == 0 && long_option_index == 3) || opt == 'h') { 75 | usage(stdout, argv[0], 0); 76 | } 77 | // handle invalid option 78 | else { 79 | fprintf(stderr, "Unrecognized option: %s.\n", argv[optind]); 80 | usage(stderr, argv[0], EXIT_FAILURE); 81 | } 82 | } 83 | 84 | if (option_output_filename == NULL) { 85 | fprintf(stderr, "Missing required argument: -o/--option\n"); 86 | usage(stderr, argv[0], EXIT_FAILURE); 87 | } 88 | 89 | FILE* output_file = fopen(option_output_filename, "w"); 90 | if (!output_file) { 91 | fprintf(stderr, "Could not open file for writing: %s\n", option_output_filename); 92 | exit(EXIT_FAILURE); 93 | } 94 | 95 | mc_writer_write_empty(&superblock, output_file); 96 | 97 | fclose(output_file); 98 | if (option_output_filename) 99 | free(option_output_filename); 100 | 101 | return EXIT_SUCCESS; 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ps2mcfs 2 | ![Tests status](https://github.com/franciscoda/ps2mcfs/actions/workflows/test.yml/badge.svg) 3 | 4 | FUSE driver for PlayStation 2 Virtual Memory Card (VMC) images. This will allow you to mount VMC images from Open PS2 Loader or PCSX2 in userspace. 5 | 6 | Implemented filesystem operations: 7 | * read 8 | * stat 9 | * readdir 10 | * mkdir 11 | * utimens 12 | * create 13 | * write 14 | * rmdir 15 | * unlink 16 | * rename 17 | 18 | The implemented operations allow most read/write commands: mkdir, touch, cat, less, rm, mv, etc. 19 | 20 | 21 | ### Mounting memory card files 22 | 23 | The following command can be used to mount a memory card file into a directory mountpoint 24 | ``` 25 | Usage: bin/fuseps2mc [OPTIONS] 26 | Mounts a Sony PlayStation 2 memory card image as a local filesystem in userspace 27 | 28 | fuseps2mcfs options: 29 | -S sync filesystem changes to the memorycard file 30 | 31 | Options: 32 | -h --help print help 33 | -V --version print version 34 | -f foreground operation 35 | ... 36 | ``` 37 | 38 | The only specific flag is `-S` which allows the program to save the filesystem changes into the memory card file. 39 | Please note that ps2mcfs is still in early development, so the use of this flag is discouraged as it may cause file corruption. 40 | 41 | Also, some filesystem status considerations: 42 | * access times are missing (they're not supported by the PS2 filesystem specification). Files will show as being last accessed in Jan 1st of 1970 43 | * user/group ownership is missing (not supported either). Files will appear as being owned by the same user and group that mounted the filesystem 44 | * Per-file permissions are supported, but not umasks. Newly created files will appear as having the most permissive combination of permissions from the umask that FUSE provides 45 | 46 | ### Obtaining memory card files 47 | 48 | There are several ways you can obtain or create memory card images: 49 | 50 | * A PS2 virtual memory card image can be obtained from real hardware by storing it into a USB drive using [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader) (you will need a PS2 capable of running homebrew applications) 51 | * A memory card file can be obtained using the PCSX2 emulator, by copying the `.ps2` files from `~/.config/PCSX2/memcards`. It's strongly recommended that you format the memory card using the PS2 browser if it's not already formatted. 52 | * You can create a formatted memory card file using the `mkfs.ps2` binary included in this project (see below) 53 | 54 | The following command can be used to create an empty memory card image: 55 | ``` 56 | Usage: bin/mkfs.ps2 -o OUTPUT_FILE [-s SIZE] [-e] [-h] 57 | Create a virtual memory card image file. 58 | 59 | -s, --size=NUM Set the memory card size in megabytes (options: 8) 60 | -e, --ecc Add ECC bytes to the generated file 61 | -o, --output=FILE Set the output file 62 | -h, --help Show this help 63 | ``` 64 | 65 | It's worth noting that PCSX2 `.ps2` files include error correcting codes (ECC data), while Open PS2 Loader `.vmc` files usually don't. 66 | 67 | This means that you will want to use the following command to generate memory cards for OPL: 68 | ```sh 69 | bin/mkfs.ps2 -o SLES-XXX.vmc -s 8 70 | ``` 71 | And the following command for PCSX2: 72 | ```sh 73 | bin/mkfs.ps2 -o Mcd002.ps2 -s 8 -e 74 | ``` 75 | 76 | ### Building 77 | 78 | The following packages are needed to build the project in Ubuntu: 79 | * `libfuse3-dev` 80 | * `pkgconf` 81 | * `clang-tools` 82 | 83 | Submodules must be initialized and updated to fetch dependencies on external libraries: `git submodule init && git submodule update -f` 84 | 85 | The executables can then be built by invoking `make` 86 | 87 | ### See also 88 | 89 | [PlayStation 2 Memory Card File System](http://www.csclub.uwaterloo.ca:11068/mymc/ps2mcfs.html) 90 | 91 | [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader) 92 | 93 | [PS2 Homebrew & emu scene](http://psx-scene.com/forums/ps2-homebrew-dev-emu-scene/) 94 | 95 | [PS2 NBD server plugin](https://github.com/bignaux/lwNBD/blob/main/plugins/mcman/lwnbd-mcman-plugin.md) allows accessing VMC files through your PS2 96 | 97 | [PS2iconsys](https://github.com/ticky/ps2iconsys) allows converting PS2 icons into their respective geometry and texture files and viceversa 98 | 99 | [µnit](https://nemequ.github.io/munit/) Unit testing framework -------------------------------------------------------------------------------- /src/tests.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "mc_writer.h" 5 | #include "ps2mcfs.h" 6 | #include "vmc_types.h" 7 | #include "utils.h" 8 | 9 | 10 | size_t count_occupied_clusters(struct vmc_meta* vmc_meta) { 11 | size_t result = 0; 12 | for (cluster_t i = 0; i < vmc_meta->superblock.last_allocatable; i++) { 13 | if (fat_get_table_entry(vmc_meta, i).entry.occupied) 14 | result += 1; 15 | } 16 | return result; 17 | } 18 | 19 | 20 | static MunitResult test_new_empty_card_with_ecc(const MunitParameter params[], void* data) { 21 | struct vmc_meta* vmc_meta = data; 22 | 23 | dir_entry_t dirent; 24 | 25 | // check that everything looks OK for the first direntry (the `.` dummy entry) 26 | ps2mcfs_get_child(vmc_meta, vmc_meta->superblock.root_cluster, 0, &dirent); 27 | munit_assert_int(dirent.cluster, ==, 0); 28 | munit_assert_ulong(dirent.length, ==, 2); 29 | munit_assert_string_equal(dirent.name, "."); 30 | 31 | // ditto for the `..` dummy entry 32 | ps2mcfs_get_child(vmc_meta, vmc_meta->superblock.root_cluster, 1, &dirent); 33 | munit_assert_int(dirent.cluster, ==, 0); 34 | munit_assert_string_equal(dirent.name, ".."); 35 | 36 | // the number of occupied clusters must be equal to the number of clusters occupied by 2 dirents 37 | size_t occupied_clusters = count_occupied_clusters(vmc_meta); 38 | size_t expected_occupied_clusters = div_ceil(2 * sizeof(dirent), vmc_meta->superblock.page_size * vmc_meta->superblock.pages_per_cluster); 39 | munit_assert_long(occupied_clusters, ==, expected_occupied_clusters); 40 | 41 | return MUNIT_OK; 42 | } 43 | 44 | 45 | static MunitResult test_fat_truncate(const MunitParameter params[], void* data) { 46 | struct vmc_meta* vmc_meta = data; 47 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 1); 48 | 49 | // extend 50 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 2); 51 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 2); 52 | // no-op 53 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 2); 54 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 2); 55 | 56 | // extend 57 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 5); 58 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 5); 59 | // no-op 60 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 5); 61 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 5); 62 | // shrink 63 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 2); 64 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 2); 65 | // return to original size 66 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 1); 67 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 1); 68 | // no-op 69 | fat_truncate(vmc_meta, vmc_meta->superblock.root_cluster, 1); 70 | munit_assert_long(count_occupied_clusters(vmc_meta), ==, 1); 71 | return MUNIT_OK; 72 | } 73 | 74 | 75 | static void* fixture_memory_card_with_ecc_setup(const MunitParameter params[], void* user_data) { 76 | (void) params; 77 | 78 | struct vmc_meta* vmc_meta = malloc(sizeof(struct vmc_meta)); 79 | vmc_meta->file = fmemopen(NULL, 8650752, "w+");// 8MB card 80 | superblock_t superblock = DEFAULT_SUPERBLOCK; 81 | superblock.card_flags |= CF_USE_ECC; 82 | mc_writer_write_empty(&superblock, vmc_meta->file); 83 | 84 | vmc_meta->ecc_bytes = 12; 85 | vmc_meta->page_spare_area_size = 16; 86 | fseek(vmc_meta->file, 0, SEEK_SET); 87 | fread(&vmc_meta->superblock, sizeof(vmc_meta->superblock), 1, vmc_meta->file); 88 | return vmc_meta; 89 | } 90 | 91 | static void fixture_vmc_meta_teardown(void* fixture) { 92 | struct vmc_meta* vmc_meta = fixture; 93 | fclose(vmc_meta->file); 94 | free(vmc_meta); 95 | } 96 | 97 | static MunitTest test_suite_tests[] = { 98 | { (char*) "/mkfsps2", test_new_empty_card_with_ecc, fixture_memory_card_with_ecc_setup, fixture_vmc_meta_teardown, MUNIT_TEST_OPTION_NONE, NULL }, 99 | { (char*) "/fat/truncate", test_fat_truncate, fixture_memory_card_with_ecc_setup, fixture_vmc_meta_teardown, MUNIT_TEST_OPTION_NONE, NULL }, 100 | 101 | { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } 102 | }; 103 | 104 | 105 | static const MunitSuite test_suite = { 106 | (char*) "", test_suite_tests, NULL, 1, MUNIT_SUITE_OPTION_NONE 107 | }; 108 | 109 | /* This is only necessary for EXIT_SUCCESS and EXIT_FAILURE */ 110 | #include 111 | 112 | int main(int argc, char* argv[MUNIT_ARRAY_PARAM(argc + 1)]) { 113 | return munit_suite_main(&test_suite, NULL, argc, argv); 114 | } 115 | -------------------------------------------------------------------------------- /src/vmc_types.h: -------------------------------------------------------------------------------- 1 | #ifndef _VMC_TYPES_H_ 2 | #define _VMC_TYPES_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | typedef uint32_t cluster_t; // cluster index 10 | 11 | union fat_entry { 12 | struct { 13 | cluster_t next_cluster: 31; // next cluster index in the linked list 14 | cluster_t occupied: 1; // when flag is 1, the cluster at this index is allocated with data 15 | } entry; 16 | cluster_t raw; // the raw 32-bit value for convenient access 17 | }; // type of an entry in the FAT table for a cluster 18 | 19 | typedef uint32_t physical_offset_t; // absolute physical offset in bytes from the start of the memory card 20 | typedef uint32_t logical_offset_t; // logical offset in bytes relative from the start of a cluster, file or directory entry 21 | 22 | // A sentinel value to denote an invalid cluster index 23 | static const cluster_t CLUSTER_INVALID = 0xFFFFFFFF; 24 | 25 | // The sentinel value used to denote the end of a linked list in the FAT table 26 | static const union fat_entry FAT_ENTRY_TERMINATOR = { .raw = CLUSTER_INVALID }; 27 | 28 | // Value used to mark fat entries as unassigned 29 | static const union fat_entry FAT_ENTRY_FREE = { .entry = { .next_cluster = 0, .occupied = 0} }; 30 | 31 | typedef struct { 32 | char _unused; 33 | uint8_t second; 34 | uint8_t minute; 35 | uint8_t hour; 36 | uint8_t day; 37 | uint8_t month; 38 | uint16_t year; 39 | } date_time_t; 40 | 41 | typedef struct { 42 | uint16_t mode; 43 | uint16_t _unused0; 44 | uint32_t length; // Length in bytes if a file, or entries if a directory. 45 | date_time_t creation; 46 | cluster_t cluster; // First cluster of the file, or 0xFFFFFFFF for an empty file. In "." entries this the first cluster of this directory's parent directory instead 47 | uint32_t dir_entry; // Only in "." entries. Entry of this directory in its parent's directory. 48 | date_time_t modification; 49 | uint32_t attributes; 50 | char _unused1[28]; 51 | char name[32]; 52 | char _unused2[416]; 53 | } dir_entry_t; 54 | 55 | typedef struct { 56 | dir_entry_t dirent; 57 | dir_entry_t parent; // parent dirent 58 | size_t index; 59 | } browse_result_t; 60 | 61 | enum card_flags { 62 | CF_USE_ECC = 0x01, // Card supports ECC. 63 | CF_BAD_BLOCK = 0x08, // Card may have bad blocks. 64 | CF_ERASE_ZEROES = 0x10, // Erased blocks have all bits set to zero. 65 | }; 66 | 67 | enum directory_flags { 68 | DF_READ = 0x0001, // Read permission 69 | DF_WRITE = 0x0002, // Write permission 70 | DF_EXECUTE = 0x0004, // Execute permission (unused) 71 | DF_PROTECTED = 0x0008, // Directory is copy protected (Meaningful only to the browser) 72 | DF_FILE = 0x0010, // Regular file 73 | DF_DIRECTORY = 0x0020, // Directory 74 | DF_0400 = 0x0400, // Set when files and directories are created, otherwise ignored 75 | DF_EXISTS = 0x8000, 76 | DF_HIDDEN = 0x2000 77 | }; 78 | 79 | typedef struct { 80 | char magic[40]; // "Sony PS2 Memory Card Format " 81 | uint16_t page_size; 82 | uint16_t pages_per_cluster; 83 | uint16_t pages_per_block; 84 | uint16_t _unused0; 85 | uint32_t clusters_per_card; 86 | cluster_t first_allocatable; // Cluster offset of the first allocatable cluster. Cluster values in the FAT and directory entries are relative to this. 87 | cluster_t last_allocatable; // The cluster after the highest allocatable cluster. Relative to alloc_offset. 88 | cluster_t root_cluster; // First cluster of the root directory. Relative to alloc_offset. 89 | uint32_t backup_block1; // Erase block used as a backup area during programming. 90 | uint32_t backup_block2; // This block should be erased to all ones. 91 | char _unused1[8]; 92 | uint32_t indirect_fat_clusters[32]; // List of indirect FAT clusters indexes (relative to the start of the card) 93 | cluster_t bad_block_list[32]; // List of erase blocks that have errors and shouldn't be used. 94 | uint8_t type; // Memory card type (Must be 2, indicating that this is a PS2 memory card.) 95 | uint8_t card_flags; // Physical characteristics of the memory card. Allowed flags: CF_USE_ECC, CF_BAD_BLOCK, CF_ERASE_ZEROES 96 | } superblock_t; 97 | 98 | static const superblock_t DEFAULT_SUPERBLOCK = { 99 | .magic = "Sony PS2 Memory Card Format 1.2.0.0\0\0\0\0", 100 | .page_size = 512, 101 | .pages_per_cluster = 2, 102 | .pages_per_block = 16, 103 | ._unused0 = 0xFF00, 104 | .clusters_per_card = 8192, // adjust per card size 105 | .first_allocatable = 41, 106 | .last_allocatable = 8135, 107 | .root_cluster = 0, 108 | .backup_block1 = 1023, 109 | .backup_block2 = 1022, 110 | ._unused1 = "\0\0\0\0\0\0\0\0", // 8 bytes 111 | // 32 items (only 1 indirect fat index) 112 | .indirect_fat_clusters = {8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0,}, 113 | // 32 items (no bad blocks) 114 | .bad_block_list = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,-1, -1, -1, -1, -1, -1, -1, -1,}, 115 | .type = 2, 116 | .card_flags = 0x2a // ecc disabled 117 | }; 118 | 119 | struct vmc_meta { 120 | superblock_t superblock; 121 | FILE* file; 122 | //void* raw_data; 123 | size_t page_spare_area_size; 124 | uint8_t ecc_bytes; 125 | }; 126 | 127 | #endif 128 | -------------------------------------------------------------------------------- /src/mc_writer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "mc_writer.h" 6 | #include "ps2mcfs.h" // ps2mcfs_time_to_date_time 7 | #include "vmc_types.h" 8 | #include "ecc.h" 9 | #include "utils.h" 10 | 11 | 12 | int mc_writer_write_empty(const superblock_t* superblock, FILE* output_file) { 13 | size_t physical_page_size = superblock->page_size; 14 | if (superblock->card_flags & CF_USE_ECC) { 15 | physical_page_size += PAGE_SPARE_PART_SIZE; // account byte spare area 16 | } 17 | uint8_t* page_buffer = malloc(physical_page_size); 18 | const time_t the_time = time(NULL); 19 | date_time_t datetime; 20 | ps2mcfs_time_to_date_time(the_time, &datetime); 21 | 22 | const dir_entry_t ROOT_DIR_ENTRIES[] = { 23 | { 24 | .mode = DF_DIRECTORY | DF_EXISTS | DF_READ | DF_WRITE | DF_EXECUTE | DF_0400, 25 | .cluster = 0, 26 | .dir_entry = 0, 27 | .length = 2, // Two entries: "." and ".." 28 | .attributes = 0, 29 | .name = ".", 30 | .creation = datetime, 31 | .modification = datetime, 32 | }, 33 | { 34 | .mode = DF_DIRECTORY | DF_EXISTS | DF_WRITE | DF_EXECUTE | DF_0400 | DF_HIDDEN, 35 | .cluster = 0, 36 | .length = 0, 37 | .length = 0, 38 | .attributes = 0, 39 | .name = "..", 40 | .creation = datetime, 41 | .modification = datetime, 42 | } 43 | }; 44 | 45 | const unsigned dirents_per_page = superblock->page_size / sizeof(dir_entry_t); 46 | const unsigned words_per_cluster = superblock->page_size * superblock->pages_per_cluster / sizeof(uint32_t); 47 | const unsigned clusters_per_block = superblock->pages_per_block / superblock->pages_per_cluster; 48 | unsigned root_directory_entries_written = 0; 49 | unsigned indirect_fat_entries_written = 0; 50 | unsigned fat_entries_written = 0; 51 | unsigned allocatable_pages_written = 0; 52 | const unsigned max_fat_entries = superblock->last_allocatable; 53 | const unsigned max_indirect_fat_entries = div_ceil(max_fat_entries, words_per_cluster); 54 | const unsigned max_indirect_fat_clusters = div_ceil(max_indirect_fat_entries, words_per_cluster); 55 | #define DEBUG_LOG(fmt, ...) \ 56 | DEBUG_printf( \ 57 | "[off: 0x%06lx clus: %4lu block: %4lu] " fmt "\n", \ 58 | ftell(output_file), \ 59 | ftell(output_file) / physical_page_size / superblock->pages_per_cluster, \ 60 | ftell(output_file) / physical_page_size / superblock->pages_per_block __VA_OPT__(,) \ 61 | __VA_ARGS__ \ 62 | ) 63 | 64 | #define WRITE_ECC() if (superblock->card_flags & CF_USE_ECC) { \ 65 | memset(page_buffer + superblock->page_size, 0, PAGE_SPARE_PART_SIZE); \ 66 | ecc512_calculate(page_buffer + superblock->page_size, page_buffer); \ 67 | } 68 | 69 | DEBUG_LOG("Writing superblock"); 70 | memset(page_buffer, 0xFF, physical_page_size); 71 | memcpy(page_buffer, superblock, sizeof(superblock_t)); 72 | WRITE_ECC(); 73 | fwrite(page_buffer, physical_page_size, 1, output_file); 74 | 75 | // fill the rest of the pages of the block with 0xFF plus ECC data 76 | memset(page_buffer, 0xFF, physical_page_size); 77 | WRITE_ECC(); 78 | for (int i = 0; i < superblock->pages_per_block - 1; i++) 79 | fwrite(page_buffer, physical_page_size, 1, output_file); 80 | 81 | DEBUG_printf("Max indirect FAT table entries: %d\n", max_indirect_fat_entries); 82 | while (indirect_fat_entries_written < max_indirect_fat_entries) { 83 | DEBUG_LOG("Writing indirect FAT table clusters (%u / %u)", indirect_fat_entries_written + 1, max_indirect_fat_entries); 84 | memset(page_buffer, 0xFF, physical_page_size); 85 | for (int i = 0; i < superblock->page_size / sizeof(uint32_t) && indirect_fat_entries_written < max_indirect_fat_entries; ++i, ++indirect_fat_entries_written) { 86 | // add an offset corresponding to 1 block (the superblock) 87 | uint32_t fat_cluster = clusters_per_block + max_indirect_fat_clusters + indirect_fat_entries_written; 88 | memcpy(page_buffer + i * sizeof(fat_cluster), &fat_cluster, sizeof(fat_cluster)); 89 | } 90 | WRITE_ECC(); 91 | fwrite(page_buffer, physical_page_size, 1, output_file); 92 | } 93 | while (ftell(output_file) / physical_page_size / superblock->pages_per_cluster < clusters_per_block + max_indirect_fat_clusters) { 94 | memset(page_buffer, 0xFF, physical_page_size); 95 | WRITE_ECC(); 96 | fwrite(page_buffer, physical_page_size, 1, output_file); 97 | } 98 | 99 | while (fat_entries_written < max_fat_entries) { 100 | DEBUG_LOG("Writing FAT table (%u / %u)", fat_entries_written + 1, max_fat_entries); 101 | memset(page_buffer, 0xFF, physical_page_size); 102 | for (int i = 0; i < superblock->page_size / sizeof(union fat_entry) && fat_entries_written < max_fat_entries; ++i, ++fat_entries_written) { 103 | union fat_entry entry = {.entry = {.occupied = 0, .next_cluster = CLUSTER_INVALID}}; 104 | if (fat_entries_written == 0) { 105 | entry.entry.occupied = 1; 106 | } 107 | memcpy(page_buffer + i * sizeof(union fat_entry), &entry, sizeof(union fat_entry)); 108 | } 109 | WRITE_ECC(); 110 | fwrite(page_buffer, physical_page_size, 1, output_file); 111 | } 112 | 113 | while (root_directory_entries_written < ROOT_DIR_ENTRIES[0].length) { 114 | DEBUG_LOG("Writing root dir entry (%u / %u)", root_directory_entries_written + 1, ROOT_DIR_ENTRIES[0].length); 115 | unsigned copy_count = MIN(dirents_per_page, ROOT_DIR_ENTRIES[0].length - root_directory_entries_written); 116 | memset(page_buffer, 0xFF, physical_page_size); 117 | memcpy(page_buffer, &ROOT_DIR_ENTRIES[root_directory_entries_written], sizeof(dir_entry_t) * copy_count); 118 | WRITE_ECC(); 119 | fwrite(page_buffer, physical_page_size, 1, output_file); 120 | root_directory_entries_written += copy_count; 121 | allocatable_pages_written += 1; 122 | } 123 | 124 | // write pages containing ECC data for the rest of the erase-block 125 | DEBUG_LOG("Writing padding data with ECC for erase block"); 126 | memset(page_buffer, 0xFF, physical_page_size); 127 | WRITE_ECC(); 128 | while (ftell(output_file) % (superblock->pages_per_block * physical_page_size) != 0) { 129 | fwrite(page_buffer, physical_page_size, 1, output_file); 130 | ++allocatable_pages_written; 131 | } 132 | 133 | DEBUG_LOG("Writing cleared allocatable clusters"); 134 | memset(page_buffer, 0xFF, physical_page_size); 135 | for (; allocatable_pages_written < superblock->last_allocatable * superblock->pages_per_cluster; allocatable_pages_written++) 136 | fwrite(page_buffer, physical_page_size, 1, output_file); 137 | 138 | DEBUG_LOG("Writing erase block2"); 139 | memset(page_buffer, 0xFF, physical_page_size); 140 | for (int i = 0; i < superblock->pages_per_block; i++) 141 | fwrite(page_buffer, physical_page_size, 1, output_file); 142 | 143 | DEBUG_LOG("Writing erase block1"); 144 | for (int i = 0; i < superblock->pages_per_block; i++) { 145 | memset(page_buffer, 0xFF, physical_page_size); 146 | // erase block1 contains a copy of the superblock 147 | if (i == 0) 148 | memcpy(page_buffer, superblock, sizeof(superblock_t)); 149 | WRITE_ECC(); 150 | fwrite(page_buffer, physical_page_size, 1, output_file); 151 | } 152 | 153 | free(page_buffer); 154 | return 0; 155 | 156 | #undef DEBUG_LOG 157 | #undef WRITE_ECC 158 | } 159 | -------------------------------------------------------------------------------- /src/fat.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "fat.h" 6 | #include "ecc.h" 7 | #include "vmc_types.h" 8 | #include "utils.h" 9 | 10 | /* file reading primitives for endianness-independent reading of structs and integers (PS2 Memory cards use little endian) */ 11 | uint32_t fread_uint32_t(FILE* f) { 12 | uint8_t result[sizeof(uint32_t)]; 13 | fread(result, sizeof(uint32_t), 1, f); 14 | return result[0] + result[1] * (1<<8) + result[2] * (1<<16) + result[3] * (1 << 24); 15 | } 16 | 17 | void fwrite_uint32_t(FILE* f, uint32_t value) { 18 | uint8_t buffer[sizeof(uint32_t)]; 19 | buffer[0] = value; 20 | buffer[1] = value / (1<<8); 21 | buffer[2] = value / (1<<16); 22 | buffer[3] = value / (1<<24); 23 | fwrite(buffer, sizeof(uint32_t), 1, f); 24 | } 25 | 26 | 27 | /* Data about the card geometry */ 28 | 29 | size_t fat_page_size(const struct vmc_meta* vmc_meta) { return vmc_meta->superblock.page_size + vmc_meta->page_spare_area_size; } 30 | size_t fat_page_capacity(const struct vmc_meta* vmc_meta) { return vmc_meta->superblock.page_size; } 31 | size_t fat_cluster_size(const struct vmc_meta* vmc_meta) { return fat_page_size(vmc_meta) * vmc_meta->superblock.pages_per_cluster; } 32 | size_t fat_cluster_capacity(const struct vmc_meta* vmc_meta) { return fat_page_capacity(vmc_meta) * vmc_meta->superblock.pages_per_cluster; } 33 | 34 | physical_offset_t fat_logical_to_physical_offset(const struct vmc_meta* vmc_meta, cluster_t cluster, logical_offset_t bytes_offset) { 35 | const size_t k_capacity = fat_cluster_capacity(vmc_meta); 36 | const size_t k_size = fat_cluster_size(vmc_meta); 37 | const size_t p_capacity = fat_page_capacity(vmc_meta); 38 | const size_t p_size = fat_page_size(vmc_meta); 39 | cluster = fat_seek(vmc_meta, cluster, bytes_offset / k_capacity); 40 | bytes_offset = bytes_offset % k_capacity; 41 | cluster += vmc_meta->superblock.first_allocatable; 42 | return cluster * k_size + bytes_offset / p_capacity * p_size; 43 | } 44 | 45 | /* R/W operations on the FAT table */ 46 | 47 | size_t fat_get_entry_offset(const struct vmc_meta* vmc_meta, cluster_t clus) { 48 | size_t cluster_capacity = fat_cluster_capacity(vmc_meta); 49 | size_t cluster_size = fat_cluster_size(vmc_meta); 50 | 51 | uint32_t k = cluster_capacity / sizeof(union fat_entry); // fat/indirfat entries per cluster (256 in a typical card) 52 | uint32_t fat_offset = clus % k; 53 | uint32_t indirect_index = clus / k; 54 | uint32_t indirect_offset = indirect_index % k; 55 | uint32_t dbl_indirect_index = indirect_index / k; 56 | uint32_t indirect_cluster_num = vmc_meta->superblock.indirect_fat_clusters[dbl_indirect_index]; 57 | 58 | 59 | uint32_t fat_cluster_offset = indirect_cluster_num * cluster_size + indirect_offset * sizeof(union fat_entry); 60 | fseek(vmc_meta->file, fat_cluster_offset, SEEK_SET); 61 | uint32_t fat_cluster_num = fread_uint32_t(vmc_meta->file); 62 | return fat_cluster_num * cluster_size + fat_offset * sizeof(union fat_entry); 63 | } 64 | 65 | union fat_entry fat_get_table_entry(const struct vmc_meta* vmc_meta, cluster_t clus) { 66 | fseek(vmc_meta->file, fat_get_entry_offset(vmc_meta, clus), SEEK_SET); 67 | union fat_entry result = {.raw = fread_uint32_t(vmc_meta->file)}; 68 | return result; 69 | } 70 | 71 | void fat_set_table_entry(const struct vmc_meta* vmc_meta, cluster_t clus, union fat_entry newval) { 72 | fseek(vmc_meta->file, fat_get_entry_offset(vmc_meta, clus), SEEK_SET); 73 | fwrite_uint32_t(vmc_meta->file, newval.raw); 74 | } 75 | 76 | cluster_t fat_allocate(const struct vmc_meta* vmc_meta, size_t len) { 77 | cluster_t start = fat_find_free_cluster(vmc_meta, 0); 78 | if (start == CLUSTER_INVALID) 79 | return CLUSTER_INVALID; 80 | fat_set_table_entry(vmc_meta, start, FAT_ENTRY_TERMINATOR); 81 | cluster_t end = fat_truncate(vmc_meta, start, len); 82 | if (end == CLUSTER_INVALID) { 83 | fat_set_table_entry(vmc_meta, start, FAT_ENTRY_FREE); 84 | return CLUSTER_INVALID; 85 | } 86 | return start; 87 | } 88 | 89 | cluster_t fat_seek(const struct vmc_meta* vmc_meta, cluster_t cluster, size_t count) { 90 | while(count > 0) { 91 | union fat_entry fat_value = fat_get_table_entry(vmc_meta, cluster); 92 | if (fat_value.raw == FAT_ENTRY_TERMINATOR.raw || !fat_value.entry.occupied) 93 | return CLUSTER_INVALID; 94 | cluster = fat_value.entry.next_cluster; 95 | --count; 96 | } 97 | return cluster; 98 | } 99 | 100 | /** 101 | * Get the last cluster in the chain 102 | */ 103 | cluster_t fat_seek_last_cluster(const struct vmc_meta* vmc_meta, cluster_t cluster) { 104 | cluster_t next_cluster = fat_seek(vmc_meta, cluster, 1); 105 | while (next_cluster != CLUSTER_INVALID) { 106 | cluster = next_cluster; 107 | next_cluster = fat_seek(vmc_meta, cluster, 1); 108 | } 109 | return cluster; 110 | } 111 | 112 | cluster_t fat_find_free_cluster(const struct vmc_meta* vmc_meta, cluster_t clus) { 113 | for (uint32_t i = 0; i < vmc_meta->superblock.last_allocatable; ++i) { 114 | cluster_t current_cluster = (clus+i) % vmc_meta->superblock.last_allocatable; 115 | union fat_entry fat_value = fat_get_table_entry(vmc_meta, current_cluster); 116 | if (fat_value.entry.occupied == 0) 117 | return current_cluster; 118 | } 119 | return CLUSTER_INVALID; 120 | } 121 | 122 | cluster_t fat_truncate(const struct vmc_meta* vmc_meta, cluster_t clus, size_t truncated_length) { 123 | union fat_entry fat_value = fat_get_table_entry(vmc_meta, clus); 124 | while (truncated_length > 1) { 125 | if (!fat_value.entry.occupied || fat_value.raw == FAT_ENTRY_TERMINATOR.raw) 126 | break; 127 | --truncated_length; 128 | clus = fat_value.entry.next_cluster; 129 | fat_value = fat_get_table_entry(vmc_meta, clus); 130 | } 131 | cluster_t last_cluster = clus; 132 | 133 | // case 0: nothing to do 134 | if (truncated_length == 1 && fat_value.raw == FAT_ENTRY_TERMINATOR.raw) 135 | return clus; 136 | 137 | // case 1: list length is greater than or equal to the desired `truncated_length` 138 | // free the rest of the list and set the terminator in `previous_cluster` if required 139 | else if (truncated_length == 0 || (truncated_length == 1 && fat_value.entry.occupied)) { 140 | while (fat_value.entry.occupied) { 141 | if (truncated_length == 1) { 142 | fat_set_table_entry(vmc_meta, clus, FAT_ENTRY_TERMINATOR); 143 | --truncated_length; 144 | } 145 | else { 146 | fat_set_table_entry(vmc_meta, clus, FAT_ENTRY_FREE); 147 | } 148 | if (fat_value.raw == FAT_ENTRY_TERMINATOR.raw) 149 | break; 150 | clus = fat_value.entry.next_cluster; 151 | fat_value = fat_get_table_entry(vmc_meta, clus); 152 | } 153 | return last_cluster; 154 | } 155 | // case 2: truncated size is greater than the size of the list 156 | // add new clusters until we reach the desired truncated length 157 | while (truncated_length > 1 && fat_value.raw == FAT_ENTRY_TERMINATOR.raw) { 158 | cluster_t new_clus = fat_find_free_cluster(vmc_meta, 0); 159 | if (new_clus == CLUSTER_INVALID) { 160 | // we might run out of space while allocating new clusters 161 | // in that case, delete the chain we just built and return 162 | fat_truncate(vmc_meta, last_cluster, 1); 163 | return CLUSTER_INVALID; 164 | } 165 | // make `clus` point to `new_clus` 166 | fat_set_table_entry(vmc_meta, clus, (union fat_entry) { .entry = { .next_cluster = new_clus, .occupied = 1} }); 167 | // make `new_clus` the new terminator of the linked list 168 | clus = new_clus; 169 | fat_value = FAT_ENTRY_TERMINATOR; 170 | fat_set_table_entry(vmc_meta, new_clus, fat_value); 171 | --truncated_length; 172 | } 173 | return clus; 174 | } 175 | 176 | /** 177 | * Copies data from the file that starts at `clus` into read_buf, then copies data from write_buf to the file. 178 | * If either read_buf or write_buf are NULL, skip their respective data copy operations 179 | */ 180 | size_t fat_rw_bytes(const struct vmc_meta* vmc_meta, cluster_t clus, logical_offset_t offset, size_t buf_size, void* restrict read_buf, const void* restrict write_buf) { 181 | if (clus == CLUSTER_INVALID) 182 | return 0; 183 | const size_t k_capacity = fat_cluster_capacity(vmc_meta); 184 | const size_t p_capacity = fat_page_capacity(vmc_meta); 185 | const size_t p_size = fat_page_size(vmc_meta); 186 | 187 | size_t buf_offset = 0; 188 | void* page_buffer = malloc(p_size); 189 | while(buf_offset < buf_size) { 190 | if (clus == CLUSTER_INVALID) 191 | break; 192 | 193 | clus = fat_seek(vmc_meta, clus, offset / k_capacity); 194 | offset %= k_capacity; 195 | const physical_offset_t mc_offset = fat_logical_to_physical_offset(vmc_meta, clus, offset); 196 | 197 | // copy until the end of the data part of the current page 198 | size_t buffer_left = buf_size - buf_offset; 199 | size_t page_left = p_capacity - offset % p_capacity; 200 | size_t s = MIN(buffer_left, page_left); 201 | 202 | physical_offset_t page_start = mc_offset / p_size * p_size; 203 | physical_offset_t spare_start = page_start + p_capacity; 204 | 205 | fseek(vmc_meta->file, page_start, SEEK_SET); 206 | fread(page_buffer, p_size, 1, vmc_meta->file); 207 | if (read_buf) { 208 | memcpy(read_buf + buf_offset, page_buffer + (mc_offset - page_start), s); 209 | if (vmc_meta->ecc_bytes == 12) { 210 | bool ecc_ok = ecc512_check(page_buffer + p_capacity, page_buffer); 211 | if (!ecc_ok) { 212 | DEBUG_printf("ECC mismatch at offset 0x%x (ECC data at: 0x%x)\n", page_start, spare_start); 213 | } 214 | } 215 | } 216 | if (write_buf) { 217 | memcpy(page_buffer + (mc_offset - page_start), write_buf + buf_offset, s); 218 | if (vmc_meta->ecc_bytes == 12) { 219 | ecc512_calculate(page_buffer + p_capacity, page_buffer); 220 | } 221 | fseek(vmc_meta->file, page_start, SEEK_SET); 222 | fwrite(page_buffer, p_size, 1, vmc_meta->file); 223 | } 224 | buf_offset += s; 225 | offset += s; 226 | } 227 | free(page_buffer); 228 | return buf_offset; 229 | } 230 | 231 | size_t fat_read_bytes(const struct vmc_meta* vmc_meta, cluster_t clus0, logical_offset_t offset, size_t size, void* buf) { 232 | return fat_rw_bytes(vmc_meta, clus0, offset, size, buf, NULL); 233 | } 234 | size_t fat_write_bytes(const struct vmc_meta* vmc_meta, cluster_t clus0, logical_offset_t offset, size_t size, const void* buf) { 235 | return fat_rw_bytes(vmc_meta, clus0, offset, size, NULL, buf); 236 | } 237 | -------------------------------------------------------------------------------- /src/fuseps2mc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include // EEXIST, ENOENT 8 | #include // NAME_MAX 9 | #include // dirname 10 | #include // RENAME_EXCHANGE, RENAME_NOREPLACE 11 | 12 | #define FUSE_USE_VERSION 30 13 | 14 | #include 15 | #include 16 | 17 | #include "vmc_types.h" 18 | #include "ps2mcfs.h" 19 | #include "utils.h" 20 | 21 | 22 | // global static instance for VMC metadata 23 | static struct vmc_meta vmc_metadata = {.superblock = {{0}}, .file = NULL, .ecc_bytes = 0, .page_spare_area_size = 0}; 24 | 25 | static void* do_init(struct fuse_conn_info* conn, struct fuse_config* cfg) { 26 | int err = ps2mcfs_get_superblock(&vmc_metadata); 27 | if (err == -1 || vmc_metadata.file == NULL) { 28 | printf("Detected error while reading superblock\n"); 29 | struct fuse_context* ctx = fuse_get_context(); 30 | fuse_exit(ctx->fuse); 31 | } 32 | return NULL; 33 | } 34 | 35 | void init_stat(struct stat* stbuf) { 36 | stbuf->st_gid = fuse_get_context()->gid; 37 | stbuf->st_uid = fuse_get_context()->uid; 38 | } 39 | 40 | static int do_getattr(const char* path, struct stat* stbuf, struct fuse_file_info* fi) { 41 | browse_result_t result; 42 | int err = ps2mcfs_browse(&vmc_metadata, NULL, path, &result); 43 | if (err) 44 | return err; 45 | init_stat(stbuf); 46 | ps2mcfs_stat(&result.dirent, stbuf); 47 | return 0; 48 | } 49 | 50 | typedef struct { 51 | void* buf; 52 | fuse_fill_dir_t filler; 53 | } readdir_args; 54 | 55 | int readdir_cb(dir_entry_t* child, void* extra) { 56 | readdir_args* args = (readdir_args*) extra; 57 | struct stat dirstat; 58 | init_stat(&dirstat); 59 | ps2mcfs_stat(child, &dirstat); 60 | return args->filler(args->buf, child->name, &dirstat, 0, 0); 61 | } 62 | 63 | static int do_readdir(const char* path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi, enum fuse_readdir_flags flags) { 64 | browse_result_t parent; 65 | ps2mcfs_browse(&vmc_metadata, NULL, path, &parent); 66 | readdir_args extra = { .buf = buf, .filler = filler }; 67 | ps2mcfs_ls(&vmc_metadata, &parent.dirent, readdir_cb, &extra); 68 | return 0; 69 | } 70 | 71 | static int do_open(const char* path, struct fuse_file_info* fi) { 72 | return ps2mcfs_browse(&vmc_metadata, NULL, path, NULL); 73 | } 74 | 75 | static int do_read(const char* path, char* buf, size_t size, off_t offset, struct fuse_file_info* fi) { 76 | browse_result_t result; 77 | ps2mcfs_browse(&vmc_metadata, NULL, path, &result); 78 | return ps2mcfs_read(&vmc_metadata, &result.dirent, buf, size, offset); 79 | } 80 | 81 | static int do_mkdir(const char* path, mode_t mode) { 82 | char dir_name[PATH_MAX]; 83 | char base_name[NAME_MAX]; 84 | strcpy(dir_name, path); 85 | strcpy(base_name, path); 86 | browse_result_t parent; 87 | int err = ps2mcfs_browse(&vmc_metadata, NULL, dirname(dir_name), &parent); 88 | if (err) 89 | return err; 90 | // use the most permissive combination of umask 91 | mode = ((mode/64)|(mode/8)|mode) & 0007; 92 | return ps2mcfs_mkdir(&vmc_metadata, &parent.dirent, basename(base_name), mode); 93 | } 94 | 95 | static int do_create(const char* path, mode_t mode, struct fuse_file_info* fi) { 96 | printf("Creating file %s\n", path); 97 | char dir_name[PATH_MAX]; 98 | char base_name[NAME_MAX]; 99 | strcpy(dir_name, path); 100 | strcpy(base_name, path); 101 | browse_result_t parent; 102 | int err = ps2mcfs_browse(&vmc_metadata, NULL, dirname(dir_name), &parent); 103 | if (err) 104 | return err; 105 | mode = ((mode / 64) | (mode / 8) | mode) & 0007; 106 | return ps2mcfs_create(&vmc_metadata, &parent.dirent, basename(base_name), CLUSTER_INVALID, mode); 107 | } 108 | 109 | static int do_utimens(const char* path, const struct timespec tv[2], struct fuse_file_info* fi) { 110 | browse_result_t result; 111 | int err = ps2mcfs_browse(&vmc_metadata, NULL, path, &result); 112 | if (err) 113 | return err; 114 | date_time_t modification; 115 | if (tv[1].tv_nsec == UTIME_OMIT) { 116 | // UTIMENSAT(2): If the tv_nsec field of one of the timespec structures has the special 117 | // value UTIME_OMIT, then the corresponding file timestamp is left unchanged 118 | return 0; 119 | } 120 | if (tv[1].tv_nsec == UTIME_NOW) { 121 | // UTIMENSAT(2): If the tv_nsec field of one of the timespec structures has the special 122 | // value UTIME_NOW, then the corresponding file timestamp is set to the current time 123 | ps2mcfs_time_to_date_time(time(NULL), &modification); 124 | } 125 | else { 126 | // 1 second = 1e9 nanoseconds 127 | ps2mcfs_time_to_date_time(tv[1].tv_nsec / 1000000000, &modification); 128 | } 129 | ps2mcfs_utime(&vmc_metadata, &result, modification); 130 | return 0; 131 | } 132 | 133 | static int do_write(const char* path, const char* data, size_t size, off_t offset, struct fuse_file_info* fi) { 134 | browse_result_t result; 135 | int err = ps2mcfs_browse(&vmc_metadata, NULL, path, &result); 136 | if (err) 137 | return err; 138 | return ps2mcfs_write(&vmc_metadata, &result, (const void*) data, size, offset); 139 | } 140 | 141 | static int do_unlink(const char* path) { 142 | browse_result_t result; 143 | int err = ps2mcfs_browse(&vmc_metadata, NULL, path, &result); 144 | if (err) 145 | return err; 146 | return ps2mcfs_unlink(&vmc_metadata, result.dirent, result.parent, result.index); 147 | } 148 | 149 | static int do_rmdir(const char* path) { 150 | browse_result_t result; 151 | int err = ps2mcfs_browse(&vmc_metadata, NULL, path, &result); 152 | if (err) 153 | return err; 154 | return ps2mcfs_rmdir(&vmc_metadata, result.dirent, result.parent, result.index); 155 | } 156 | 157 | static int do_rename(const char * path_from, const char * path_to, unsigned int flags) { 158 | browse_result_t origin, destination; 159 | int err1 = ps2mcfs_browse(&vmc_metadata, NULL, path_from, &origin); 160 | int err2 = ps2mcfs_browse(&vmc_metadata, NULL, path_to, &destination); 161 | 162 | if (err1) { 163 | return err1; 164 | } 165 | if ((flags & RENAME_NOREPLACE) && !err2) { 166 | return -EEXIST; 167 | } 168 | if ((flags & RENAME_EXCHANGE) && err2) { 169 | return -ENOENT; 170 | } 171 | // target file does not exist. create it 172 | if (err2 == -ENOENT) { 173 | char dir_name[PATH_MAX]; 174 | char base_name[NAME_MAX]; 175 | strcpy(dir_name, path_to); 176 | strcpy(base_name, path_to); 177 | // browse to the parent dir 178 | if ((err2 = ps2mcfs_browse(&vmc_metadata, NULL, dirname(dir_name), &destination))) 179 | return err2; 180 | if ((err2 = ps2mcfs_create(&vmc_metadata, &destination.dirent, basename(base_name), origin.dirent.cluster, origin.dirent.mode))) 181 | return err2; 182 | } 183 | 184 | // browse again to reload dirents and parent dirents 185 | if ((err1 = ps2mcfs_browse(&vmc_metadata, NULL, path_from, &origin))) 186 | return err1; 187 | if ((err2 = ps2mcfs_browse(&vmc_metadata, NULL, path_to, &destination))) 188 | return err2; 189 | 190 | // Swap files origin <-> destination 191 | ps2mcfs_set_child(&vmc_metadata, destination.parent.cluster, destination.index, &origin.dirent); 192 | ps2mcfs_set_child(&vmc_metadata, origin.parent.cluster, origin.index, &destination.dirent); 193 | // delete the old origin, which is now stored in destination.parent[destination.index] 194 | if (!(flags & RENAME_EXCHANGE)) { 195 | // prevent deleting the contents of the moved file in case they point to the same file contents 196 | if (origin.dirent.cluster == destination.dirent.cluster) 197 | origin.dirent.cluster = CLUSTER_INVALID; 198 | ps2mcfs_unlink(&vmc_metadata, origin.dirent, destination.parent, destination.index); 199 | } 200 | return 0; 201 | } 202 | 203 | static struct fuse_operations operations = { 204 | .init = do_init, 205 | .getattr = do_getattr, 206 | .readdir = do_readdir, 207 | .open = do_open, 208 | .read = do_read, 209 | .mkdir = do_mkdir, 210 | .create = do_create, 211 | .utimens = do_utimens, 212 | .write = do_write, 213 | .unlink = do_unlink, 214 | .rmdir = do_rmdir, 215 | .rename = do_rename, 216 | }; 217 | 218 | 219 | struct cli_options { 220 | // fuseps2mc options 221 | char* mc_path; 222 | int sync_to_fs; 223 | 224 | // standard fuse options 225 | char* mountpoint; 226 | int singlethread; 227 | int foreground; 228 | int debug; 229 | int show_version; 230 | int show_help; 231 | unsigned int max_threads; 232 | }; 233 | 234 | static const struct fuse_opt CLI_OPTIONS[] = { 235 | {.templ = "-S", .offset = offsetof(struct cli_options, sync_to_fs), .value = true}, 236 | {.templ = "-h", .offset = offsetof(struct cli_options, show_help), .value = 1}, 237 | {.templ = "--help", .offset = offsetof(struct cli_options, show_help), .value = 1}, 238 | {.templ = "-V", .offset = offsetof(struct cli_options, show_version), .value = 1}, 239 | {.templ = "--version", .offset = offsetof(struct cli_options, show_version), .value = 1}, 240 | {.templ = "-f", .offset = offsetof(struct cli_options, foreground), .value = 1}, 241 | {.templ = "-s", .offset = offsetof(struct cli_options, singlethread), .value = 1}, 242 | {.templ = "max_threads=%u", .offset = offsetof(struct cli_options, max_threads), .value = 1}, 243 | FUSE_OPT_END 244 | }; 245 | 246 | 247 | void usage(FILE* stream, const char* program_name) { 248 | fprintf( 249 | stream, 250 | "Usage: %s [OPTIONS]\n" 251 | "Mounts a Sony PlayStation 2 memory card image as a local filesystem in userspace\n" 252 | "\nfuseps2mcfs options:\n" 253 | " -S sync filesystem changes to the memorycard file\n" 254 | "\nOptions:\n" 255 | " -h --help print help\n" 256 | " -V --version print version\n" 257 | " -f foreground operation\n" 258 | " -s disable multi-threaded operation\n" 259 | " -o max_threads the maximum number of threads allowed (default: 10)\n", 260 | program_name 261 | ); 262 | } 263 | 264 | 265 | int opt_proc(void* data, const char* arg, int key, struct fuse_args* outargs) { 266 | struct cli_options* opts = data; 267 | if (key == FUSE_OPT_KEY_OPT) { // did not match any template 268 | return 1; 269 | } 270 | else if (key == FUSE_OPT_KEY_NONOPT) { 271 | if (!opts->mc_path) { 272 | char mc_path[PATH_MAX] = ""; 273 | if (realpath(arg, mc_path) == NULL) { 274 | fuse_log(FUSE_LOG_ERR, "fuse: bad mc file path `%s': %s\n", arg, strerror(errno)); 275 | return -1; 276 | } 277 | return fuse_opt_add_opt(&opts->mc_path, mc_path); 278 | } 279 | else if (!opts->mountpoint) { 280 | char mountpoint[PATH_MAX] = ""; 281 | if (realpath(arg, mountpoint) == NULL) { 282 | fuse_log(FUSE_LOG_ERR, "fuse: bad mount point `%s': %s\n", arg, strerror(errno)); 283 | return -1; 284 | } 285 | return fuse_opt_add_opt(&opts->mountpoint, mountpoint); 286 | } 287 | } 288 | return 0; 289 | } 290 | 291 | 292 | int main(int argc, char** argv) { 293 | struct fuse_args args = FUSE_ARGS_INIT(argc, argv); 294 | struct cli_options opts = { 295 | .mc_path = NULL, 296 | .sync_to_fs = 0, 297 | 298 | .mountpoint = NULL, 299 | .show_help = 0, 300 | .show_version = 0, 301 | .singlethread = 0, 302 | .foreground = 0, 303 | .max_threads = 10, 304 | }; 305 | 306 | if (fuse_opt_parse(&args, &opts, CLI_OPTIONS, opt_proc) == -1) 307 | return -1; 308 | 309 | 310 | // code below is based on the implementation of `fuse_main_real()` from libfuse 311 | int res; 312 | 313 | if (opts.show_version) { 314 | printf("FUSE library version %u.%u\n", FUSE_MAJOR_VERSION, FUSE_MINOR_VERSION); 315 | res = 0; 316 | goto out1; 317 | } 318 | 319 | if (opts.show_help) { 320 | if(args.argv[0][0] != '\0') 321 | usage(stdout, args.argv[0]); 322 | //fuse_cmdline_help(); 323 | fuse_lib_help(&args); 324 | res = 0; 325 | goto out1; 326 | } 327 | 328 | if (!opts.mc_path) { 329 | fprintf(stderr, "fuseps2mc: no memory card file specified\n"); 330 | res = 2; 331 | goto out1; 332 | } 333 | if (!opts.mountpoint) { 334 | fuse_log(FUSE_LOG_ERR, "error: no mountpoint specified\n"); 335 | res = 2; 336 | goto out1; 337 | } 338 | 339 | if (opts.sync_to_fs) { 340 | vmc_metadata.file = fopen(opts.mc_path, "rb+"); 341 | fprintf( 342 | stderr, 343 | "WARNING: Opening memory card file \"%s\" for read and write operations.\n" 344 | "This may cause data corruption as fuseps2mcfs is still in early development.\n" 345 | "Consider running without the -S flag.\n", 346 | opts.mc_path 347 | ); 348 | } 349 | else { 350 | // memorycard sync operations are disabled 351 | // create a memory buffer and copy the whole memory card file 352 | FILE* f = fopen(opts.mc_path, "rb+"); 353 | if (!f) { 354 | fprintf(stderr, "error: could not open file: %s\n", opts.mc_path); 355 | res = 2; 356 | goto out1; 357 | } 358 | fseek(f, 0, SEEK_END); 359 | size_t size = ftell(f); 360 | fseek(f, 0, SEEK_SET); 361 | vmc_metadata.file = fmemopen(NULL, size, "rb+"); 362 | 363 | while (!feof(f)) { 364 | char buffer[1024]; 365 | fwrite(buffer, 1, fread(buffer, 1, sizeof(buffer), f), vmc_metadata.file); 366 | } 367 | 368 | fclose(f); 369 | fseek(vmc_metadata.file, 0, SEEK_SET); 370 | } 371 | if (!vmc_metadata.file) { 372 | fprintf(stderr, "error: could not open file: %s\n", opts.mc_path); 373 | res = 2; 374 | goto out1; 375 | } 376 | 377 | struct fuse* fuse = fuse_new(&args, &operations, sizeof(operations), NULL); 378 | if (fuse == NULL) { 379 | res = 3; 380 | goto out1; 381 | } 382 | 383 | if (fuse_mount(fuse,opts.mountpoint) != 0) { 384 | res = 4; 385 | goto out2; 386 | } 387 | 388 | if (fuse_daemonize(opts.foreground) != 0) { 389 | res = 5; 390 | goto out3; 391 | } 392 | 393 | struct fuse_session *se = fuse_get_session(fuse); 394 | if (fuse_set_signal_handlers(se) != 0) { 395 | res = 6; 396 | goto out3; 397 | } 398 | 399 | if (opts.singlethread) 400 | res = fuse_loop(fuse); 401 | else { 402 | struct fuse_loop_config loop_config = {0}; 403 | loop_config.clone_fd = 0; 404 | loop_config.max_idle_threads = 100; 405 | #if FUSE_USE_VERSION < 32 406 | res = fuse_loop_mt(fuse, loop_config.clone_fd); 407 | #else 408 | res = fuse_loop_mt(fuse, &loop_config); 409 | #endif 410 | } 411 | if (res) 412 | res = 8; 413 | 414 | fuse_remove_signal_handlers(se); 415 | out3: 416 | fuse_unmount(fuse); 417 | out2: 418 | fuse_destroy(fuse); 419 | out1: 420 | free(opts.mountpoint); 421 | fuse_opt_free_args(&args); 422 | 423 | if (opts.mc_path != NULL) 424 | free(opts.mc_path); 425 | if (vmc_metadata.file != NULL) 426 | fclose(vmc_metadata.file); 427 | return res; 428 | } 429 | 430 | -------------------------------------------------------------------------------- /src/ps2mcfs.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "vmc_types.h" 12 | #include "fat.h" 13 | #include "ps2mcfs.h" 14 | #include "utils.h" 15 | 16 | 17 | bool ps2mcfs_is_directory(const dir_entry_t* const dirent) { 18 | return dirent->mode & DF_DIRECTORY; 19 | } 20 | bool ps2mcfs_is_file(const dir_entry_t* const dirent) { 21 | return dirent->mode & DF_FILE; 22 | } 23 | 24 | void ps2mcfs_time_to_date_time(time_t thetime, date_time_t* dt) { 25 | struct tm t; 26 | gmtime_r(&thetime, &t); 27 | dt->second = t.tm_sec; 28 | dt->minute = t.tm_min; 29 | dt->hour = t.tm_hour; 30 | dt->day = t.tm_mday; 31 | dt->month = t.tm_mon+1; 32 | dt->year = t.tm_year+1900; 33 | } 34 | time_t date_time_to_timestamp(const date_time_t* const dt) { 35 | struct tm time = { 36 | dt->second, dt->minute, dt->hour, 37 | dt->day, dt->month-1, dt->year-1900, 38 | 0, 0, 0 39 | }; 40 | return mktime(&time); 41 | } 42 | 43 | int ps2mcfs_get_superblock(struct vmc_meta* metadata_out) { 44 | if (metadata_out->file == NULL) { 45 | return -1; 46 | } 47 | fseek(metadata_out->file, 0, SEEK_END); 48 | size_t size = ftell(metadata_out->file); 49 | 50 | if (size < sizeof(superblock_t)) { 51 | // data is too small to contain a superblock 52 | fprintf(stderr, "Memory card file is to small to contain a superblock. Size: %lu. Minimum: %lu \n", size, sizeof(superblock_t)); 53 | return -1; 54 | } 55 | fseek(metadata_out->file, 0, SEEK_SET); 56 | fread(&metadata_out->superblock, sizeof(superblock_t), 1, metadata_out->file); 57 | 58 | if (strncmp(metadata_out->superblock.magic, DEFAULT_SUPERBLOCK.magic, sizeof(DEFAULT_SUPERBLOCK.magic)) != 0) { 59 | fprintf( 60 | stderr, 61 | "Magic string mismatch. Make sure the memory card was properly formatted." 62 | "Expected: \"%s\". Read: \"%.*s\"\n", 63 | DEFAULT_SUPERBLOCK.magic, 64 | (int)(sizeof(DEFAULT_SUPERBLOCK.magic) - 1), metadata_out->superblock.magic 65 | ); 66 | return -1; 67 | } 68 | const size_t expected_size = metadata_out->superblock.clusters_per_card * metadata_out->superblock.pages_per_cluster * metadata_out->superblock.page_size; 69 | const size_t expected_size_with_ecc = metadata_out->superblock.clusters_per_card * metadata_out->superblock.pages_per_cluster * (metadata_out->superblock.page_size + 16); 70 | if (size == expected_size) { 71 | metadata_out->ecc_bytes = 0; 72 | metadata_out->page_spare_area_size = 0; 73 | } 74 | else if (size == expected_size_with_ecc) { 75 | metadata_out->ecc_bytes = 12; // 3 bytes for each 128-byte chunk of a page 76 | metadata_out->page_spare_area_size = 16; 77 | } 78 | else { 79 | printf( 80 | "VMC File size mismatch: %luB\n" 81 | "\tExpected size (no ecc): %d clusters * %d pages per cluster * %d bytes per page = %luB.\n" 82 | "\tExpected size (16 byte ecc): %d clusters * %d pages per cluster * %d bytes per page = %luB.\n", 83 | size, 84 | metadata_out->superblock.clusters_per_card, 85 | metadata_out->superblock.pages_per_cluster, 86 | metadata_out->superblock.page_size, 87 | expected_size, 88 | metadata_out->superblock.clusters_per_card, 89 | metadata_out->superblock.pages_per_cluster, 90 | metadata_out->superblock.page_size + 16, 91 | expected_size_with_ecc 92 | ); 93 | return -1; 94 | } 95 | if (metadata_out->superblock.type != 2) { 96 | printf("Unknown card type: %d. (expected 2)\n", metadata_out->superblock.type); 97 | return -1; 98 | } 99 | //metadata_out->file = file; 100 | printf("Mounted card flags: %x\n", metadata_out->superblock.card_flags); 101 | return 0; 102 | } 103 | 104 | int ps2mcfs_get_child(const struct vmc_meta* vmc_meta, cluster_t clus0, unsigned int entrynum, dir_entry_t* dest) { 105 | size_t sz = fat_read_bytes(vmc_meta, clus0, entrynum * sizeof(dir_entry_t), sizeof(dir_entry_t), dest); 106 | if (sz != sizeof(dir_entry_t)) 107 | return -ENOENT; 108 | return 0; 109 | } 110 | 111 | int ps2mcfs_set_child(const struct vmc_meta* vmc_meta, cluster_t clus0, unsigned int entrynum, dir_entry_t* src) { 112 | DEBUG_printf("Updating directory entry at index %u starting from cluster %u to: \"%s\" (cluster: %u, size: %u)\n", entrynum, clus0, src->name, src->cluster, src->length); 113 | size_t sz = fat_write_bytes(vmc_meta, clus0, entrynum * sizeof(dir_entry_t), sizeof(dir_entry_t), src); 114 | if(sz != sizeof(dir_entry_t)) 115 | return -ENOENT; 116 | return 0; 117 | } 118 | 119 | void ps2mcfs_ls(const struct vmc_meta* vmc_meta, dir_entry_t* parent, int(* cb)(dir_entry_t* child, void* extra), void* extra) { 120 | size_t dirents_per_cluster = fat_cluster_capacity(vmc_meta) / sizeof(dir_entry_t); 121 | cluster_t clus = parent->cluster; 122 | for (int i = 0; i < parent->length; ++i) { 123 | if (i % dirents_per_cluster == 0 && i != 0) { 124 | if ((clus = fat_seek(vmc_meta, clus, 1)) == CLUSTER_INVALID) 125 | break; 126 | } 127 | dir_entry_t child; 128 | ps2mcfs_get_child(vmc_meta, clus, i % dirents_per_cluster, &child); 129 | 130 | if (child.mode & DF_EXISTS) { 131 | if (cb(&child, extra) != 0) 132 | break; 133 | } 134 | else { 135 | DEBUG_printf("Skipping deleted directory entry \"%s/%s\" (mode: %u, cluster: %u)\n", parent->name, child.name, child.mode, child.cluster); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Fetch the dirent that corresponds to 'path' the dirent, parent and offset is returned in the dest pointer 142 | */ 143 | int ps2mcfs_browse(const struct vmc_meta* vmc_meta, dir_entry_t* root, const char* path, browse_result_t* dest) { 144 | const size_t dirents_per_cluster = fat_cluster_capacity(vmc_meta) / sizeof(dir_entry_t); 145 | 146 | const char* slash = strchr(path, '/'); 147 | bool is_basename = false; 148 | if (!slash) { // slash not found, we're at the base file name 149 | slash = path + strlen(path); 150 | is_basename = true; 151 | } 152 | dir_entry_t dirent; 153 | if (root) { 154 | dirent = *root; 155 | } 156 | else { 157 | ps2mcfs_get_child(vmc_meta, vmc_meta->superblock.root_cluster, 0, &dirent); 158 | } 159 | 160 | cluster_t clus = dirent.cluster; 161 | size_t dirent_index = 0; 162 | 163 | if (slash != path) { 164 | if (!ps2mcfs_is_directory(&dirent)) 165 | return -ENOTDIR; 166 | else if (slash-path >= 32) 167 | return -ENAMETOOLONG; 168 | else if (slash-path == 2 && strncmp(path, "..", 2) == 0) { 169 | ps2mcfs_get_child(vmc_meta, dirent.cluster, 0, &dirent); // dirent's '.' entry 170 | ps2mcfs_get_child(vmc_meta, dirent.cluster, 0, &dirent); // get parent '.' entry 171 | ps2mcfs_get_child(vmc_meta, dirent.cluster, dirent.dir_entry, &dirent); // get parent entry from grandparent 172 | } 173 | else if (strcmp(path, ".") != 0) { 174 | for (dirent_index = 0; dirent_index < root->length; dirent_index++) { 175 | if (dirent_index % dirents_per_cluster == 0 && dirent_index != 0) 176 | clus = fat_seek(vmc_meta, clus, 1); 177 | ps2mcfs_get_child(vmc_meta, clus, dirent_index % dirents_per_cluster, &dirent); 178 | if ((dirent.mode & DF_EXISTS) && strncmp(dirent.name, path, slash-path) == 0) 179 | break; 180 | } 181 | if (dirent_index == root->length) 182 | return -ENOENT; 183 | } 184 | } 185 | if (is_basename) { 186 | if (dest) { 187 | dest->dirent = dirent; 188 | dest->parent = *root; 189 | dest->index = dirent_index; 190 | } 191 | return 0; 192 | } 193 | return ps2mcfs_browse(vmc_meta, &dirent, slash + 1, dest); 194 | } 195 | 196 | void ps2mcfs_stat(const dir_entry_t* const dirent, struct stat* stbuf) { 197 | if (ps2mcfs_is_directory(dirent)) 198 | stbuf->st_mode |= S_IFDIR; 199 | else 200 | stbuf->st_mode |= S_IFREG; 201 | stbuf->st_size = dirent->length; 202 | stbuf->st_blksize = sizeof(dir_entry_t); 203 | stbuf->st_blocks = 1; 204 | stbuf->st_mtime = date_time_to_timestamp(&dirent->creation); 205 | stbuf->st_ctime = date_time_to_timestamp(&dirent->modification); 206 | // fat32 doesn't manage per-user permissions, copy rwx permissions 207 | // across the 'user', 'group' and 'other' umasks 208 | stbuf->st_mode += (dirent->mode & 7) * 0111; 209 | } 210 | 211 | int ps2mcfs_read(const struct vmc_meta* vmc_meta, const dir_entry_t* dirent, void* buf, size_t size, off_t offset) { 212 | if (offset > dirent->length) 213 | return 0; 214 | if (offset + size > dirent->length) 215 | size = dirent->length-offset; 216 | return fat_read_bytes(vmc_meta, dirent->cluster, offset, size, buf); 217 | } 218 | 219 | int ps2mcfs_add_child(const struct vmc_meta* vmc_meta, dir_entry_t* parent, dir_entry_t* new_child) { 220 | DEBUG_printf("Adding new child \"%s/%s\"\n", parent->name, new_child->name); 221 | const size_t dirents_per_cluster = fat_cluster_capacity(vmc_meta) / sizeof(dir_entry_t); 222 | const size_t new_size = div_ceil(parent->length + 1, dirents_per_cluster); 223 | cluster_t last = fat_truncate(vmc_meta, parent->cluster, new_size); 224 | if (last == CLUSTER_INVALID) 225 | return -ENOSPC; 226 | 227 | ps2mcfs_set_child(vmc_meta, parent->cluster, parent->length, new_child); 228 | dir_entry_t dummy; 229 | parent->length++; 230 | // now need to write the updated parent length into the parent's dir entry 231 | ps2mcfs_get_child(vmc_meta, parent->cluster, 0, &dummy); // read the parent's `.` entry (which points to its parent) 232 | ps2mcfs_set_child(vmc_meta, dummy.cluster, dummy.dir_entry, parent); // write the updated `parent` dirent 233 | return 0; 234 | } 235 | 236 | void ps2mcfs_utime(const struct vmc_meta* vmc_meta, browse_result_t* dirent, date_time_t modification) { 237 | dirent->dirent.modification = modification; 238 | dirent->dirent.creation = modification; // probably not ok 239 | ps2mcfs_set_child(vmc_meta, dirent->parent.cluster, dirent->index, &dirent->dirent); 240 | } 241 | 242 | int ps2mcfs_mkdir(const struct vmc_meta* vmc_meta, dir_entry_t* parent, const char* name, uint16_t mode) { 243 | size_t dirents_per_cluster = fat_cluster_capacity(vmc_meta)/sizeof(dir_entry_t); 244 | dir_entry_t new_child; 245 | new_child.mode = mode | DF_DIRECTORY | DF_EXISTS; 246 | new_child.length = 2; 247 | ps2mcfs_time_to_date_time(time(NULL), &new_child.creation); 248 | new_child.cluster = fat_allocate(vmc_meta, div_ceil(2, dirents_per_cluster)); 249 | new_child.modification = new_child.creation; 250 | new_child.attributes = 0; 251 | strcpy(new_child.name, name); 252 | 253 | if (new_child.cluster == CLUSTER_INVALID) { 254 | return -ENOSPC; 255 | } 256 | 257 | int err = ps2mcfs_add_child(vmc_meta, parent, &new_child); 258 | if (err) { 259 | fat_truncate(vmc_meta, new_child.cluster, 0); 260 | return err; 261 | } 262 | 263 | // make the '.' and '..' entries for the new child 264 | dir_entry_t dummy; 265 | // TODO: what else should be filled here? 266 | dummy.cluster = parent->cluster; 267 | dummy.dir_entry = parent->length-1; 268 | dummy.mode = new_child.mode; 269 | strcpy(dummy.name, "."); 270 | ps2mcfs_set_child(vmc_meta, new_child.cluster, 0, &dummy); 271 | strcpy(dummy.name, ".."); 272 | ps2mcfs_set_child(vmc_meta, new_child.cluster, 1, &dummy); 273 | 274 | return 0; 275 | } 276 | 277 | int ps2mcfs_create(const struct vmc_meta* vmc_meta, dir_entry_t* parent, const char* name, cluster_t cluster, uint16_t mode) { 278 | DEBUG_printf("Creating new file %s/%s, cluster: %u, mode: %03o\n", parent->name, name, cluster, mode); 279 | dir_entry_t new_child; 280 | new_child.mode = mode | DF_FILE | DF_EXISTS; 281 | new_child.length = 0; 282 | ps2mcfs_time_to_date_time(time(NULL), &new_child.creation); 283 | new_child.cluster = cluster; 284 | new_child.modification = new_child.creation; 285 | new_child.attributes = 0; 286 | strcpy(new_child.name, name); 287 | 288 | int err = ps2mcfs_add_child(vmc_meta, parent, &new_child); 289 | if (err) 290 | return err; 291 | return 0; 292 | } 293 | 294 | int ps2mcfs_write(const struct vmc_meta* vmc_meta, const browse_result_t* dirent, const void* buf, size_t size, off_t offset) { 295 | if (offset + size > dirent->dirent.length) { 296 | dir_entry_t new_entry = dirent->dirent; 297 | if (new_entry.cluster == CLUSTER_INVALID) { 298 | new_entry.cluster = fat_allocate(vmc_meta, 1); 299 | } 300 | size_t k = fat_cluster_capacity(vmc_meta); 301 | size_t needed_clusters = div_ceil(offset+size, k); 302 | cluster_t clusn = fat_truncate(vmc_meta, new_entry.cluster, needed_clusters); 303 | if (clusn == CLUSTER_INVALID) 304 | return -ENOSPC; 305 | new_entry.length = offset + size; 306 | ps2mcfs_set_child(vmc_meta, dirent->parent.cluster, dirent->index, &new_entry); 307 | return fat_write_bytes(vmc_meta, new_entry.cluster, offset, size, buf); 308 | } 309 | return fat_write_bytes(vmc_meta, dirent->dirent.cluster, offset, size, buf); 310 | } 311 | 312 | int ps2mcfs_unlink(const struct vmc_meta* vmc_meta, const dir_entry_t unlinked_file, const dir_entry_t parent, size_t index_in_parent) { 313 | DEBUG_printf("Unlinking file %s/%s index %lu/%u\n", parent.name, unlinked_file.name, index_in_parent, parent.length - 1); 314 | // free all the clusters in the deleted file (if it's not empty) 315 | if (unlinked_file.cluster != CLUSTER_INVALID) 316 | fat_truncate(vmc_meta, unlinked_file.cluster, 0); 317 | 318 | dir_entry_t temp; 319 | // remove the dirent in the parents list of dirents by shifting the siblings 320 | for (int i = index_in_parent; i < parent.length - 1; ++i) { 321 | ps2mcfs_get_child(vmc_meta, parent.cluster, i + 1, &temp); 322 | ps2mcfs_set_child(vmc_meta, parent.cluster, i, &temp); 323 | // the reverse link subdirectory entry (".") also has to be updated in each shifted sibling 324 | // to reflect its new position in the parent's list of entries 325 | cluster_t cluster = temp.cluster; 326 | ps2mcfs_get_child(vmc_meta, cluster, 0, &temp); 327 | temp.dir_entry = i; 328 | ps2mcfs_set_child(vmc_meta, cluster, 0, &temp); 329 | } 330 | 331 | // we now need to decrement the length of the parent dir entry 332 | // and update that inside its parent 333 | ps2mcfs_get_child(vmc_meta, parent.cluster, 0, &temp); // dirent's '.' entry 334 | cluster_t parent_parent_cluster = temp.cluster; // get the cluster of `parent` parent 335 | size_t parent_index_in_parent = temp.dir_entry; // get index of `parent` direntry in its parent directory 336 | temp = parent; 337 | --temp.length; 338 | ps2mcfs_set_child(vmc_meta, parent_parent_cluster, parent_index_in_parent, &temp); 339 | return 0; 340 | } 341 | 342 | int ps2mcfs_rmdir(const struct vmc_meta* vmc_meta, const dir_entry_t removed_dir, const dir_entry_t parent, size_t index_in_parent) { 343 | // Remove children starting at index=2 (skip `.` and `..` dummy entries) 344 | for (int i = 2; i < removed_dir.length; ++i) { 345 | // get and free all the clusters 346 | dir_entry_t child; 347 | ps2mcfs_get_child(vmc_meta, removed_dir.cluster, i, &child); 348 | if (child.mode & DF_DIRECTORY) { 349 | ps2mcfs_rmdir(vmc_meta, removed_dir, child, i); 350 | } 351 | fat_truncate(vmc_meta, child.cluster, 0); 352 | } 353 | // NOTE: The above is not stricly necessary as rm -r calls unlink() for each file before calling rmdir 354 | 355 | return ps2mcfs_unlink(vmc_meta, removed_dir, parent, index_in_parent); 356 | } 357 | --------------------------------------------------------------------------------