From 6d4b2593241ef9d0d5d49a3b57a43d4da0150a32 Mon Sep 17 00:00:00 2001 From: mtheall Date: Sun, 23 Nov 2014 16:39:00 -0600 Subject: [PATCH] initial commit --- .gitignore | 4 + Makefile | 168 +++ data/sans.10.kerning.bin | Bin 0 -> 576 bytes data/sans.10.render.bin | Bin 0 -> 6836 bytes data/sans.12.kerning.bin | Bin 0 -> 1164 bytes data/sans.12.render.bin | Bin 0 -> 9592 bytes data/sans.14.kerning.bin | Bin 0 -> 1320 bytes data/sans.14.render.bin | Bin 0 -> 11768 bytes data/sans.16.kerning.bin | Bin 0 -> 1644 bytes data/sans.16.render.bin | Bin 0 -> 14696 bytes data/sans.8.kerning.bin | Bin 0 -> 336 bytes data/sans.8.render.bin | Bin 0 -> 5544 bytes ftbrony.png | Bin 0 -> 6575 bytes include/console.h | 12 + include/debug.h | 54 + include/ftp.h | 5 + source/console.c | 735 +++++++++++++ source/ftp.c | 2136 ++++++++++++++++++++++++++++++++++++++ source/main.c | 114 ++ 19 files changed, 3228 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 data/sans.10.kerning.bin create mode 100644 data/sans.10.render.bin create mode 100644 data/sans.12.kerning.bin create mode 100644 data/sans.12.render.bin create mode 100644 data/sans.14.kerning.bin create mode 100644 data/sans.14.render.bin create mode 100644 data/sans.16.kerning.bin create mode 100644 data/sans.16.render.bin create mode 100644 data/sans.8.kerning.bin create mode 100644 data/sans.8.render.bin create mode 100644 ftbrony.png create mode 100644 include/console.h create mode 100644 include/debug.h create mode 100644 include/ftp.h create mode 100644 source/console.c create mode 100644 source/ftp.c create mode 100644 source/main.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4ac6ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +*.3dsx +*.smdh +*.elf diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4ab7f04 --- /dev/null +++ b/Makefile @@ -0,0 +1,168 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +CTRULIB:=/home/mtheall/workspace/ninjhax/ctrulib/libctru + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +ifeq ($(strip $(CTRULIB)),) +# THIS IS TEMPORARY - in the future it should be at $(DEVKITPRO)/libctru +$(error "Please set CTRULIB in your environment. export CTRULIB=libctru") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITARM)/3ds_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# +# NO_SMDH: if set to anything, no SMDH file is generated. +# APP_TITLE is the name of the app stored in the SMDH file (Optional) +# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) +# APP_AUTHOR is the author of the app stored in the SMDH file (Optional) +# ICON is the filename of the icon (.png), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .png +# - icon.png +# - /default_icon.png +#--------------------------------------------------------------------------------- +TARGET := $(notdir $(CURDIR)) +BUILD := build +SOURCES := source +DATA := data +INCLUDES := include + +APP_TITLE := ftBRONY +APP_DESCRIPTION := Like ftPONY but magical. +APP_AUTHOR := mtheall + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=softfp + +CFLAGS := -g -Wall -O3 -mword-relocations \ + -fomit-frame-pointer -ffast-math \ + $(ARCH) \ + -DSTATUS_STRING="\"ftpd v1.0\"" + +CFLAGS += $(INCLUDE) -DARM11 -D_3DS + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(TARGET).map + +LIBS := -lctru + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(CTRULIB) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) + export LD := $(CC) +else + export LD := $(CXX) +endif +#--------------------------------------------------------------------------------- + +export OFILES := $(addsuffix .o,$(BINFILES)) \ + $(CPPFILES:.cpp=.o) \ + $(CFILES:.c=.o) \ + $(SFILES:.s=.o) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.png) + ifneq (,$(findstring $(TARGET).png,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).png + else + ifneq (,$(findstring icon.png,$(icons))) + export APP_ICON := $(TOPDIR)/icon.png + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +.PHONY: $(BUILD) clean all + +#--------------------------------------------------------------------------------- +all: $(BUILD) + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @make --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf + + +#--------------------------------------------------------------------------------- +else + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +ifeq ($(strip $(NO_SMDH)),) +.PHONY: all +all : $(OUTPUT).3dsx $(OUTPUT).smdh +endif +$(OUTPUT).3dsx: $(OUTPUT).elf +$(OUTPUT).elf: $(OFILES) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o: %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/data/sans.10.kerning.bin b/data/sans.10.kerning.bin new file mode 100644 index 0000000000000000000000000000000000000000..f6b70eb07b317377a3a7d639a3e02c204993ccb1 GIT binary patch literal 576 zcmZ9|F$%&!6h+b5P+WkWjg1x-qF9Qg78I52&Yg|_!d!S*gd84|KksL1tsU$6_THy@ zUxqi4WAkl!HO}C5m~+e_zlN9LUA?)3GuV6ge?E;fC75;?n$ximc_Q7_*-iy7Lf8e+L19!xkJCocUvGcL>vGcM0+0U8p JFguwq;U5^M(hvXu literal 0 HcmV?d00001 diff --git a/data/sans.10.render.bin b/data/sans.10.render.bin new file mode 100644 index 0000000000000000000000000000000000000000..08da873cf6202a3fa4ac3c7a3b6ae34e6911e95c GIT binary patch literal 6836 zcma)A2V7T25+00nRM3E)5~WB{vBY>9�Jqm!Pq0?1IMFv7R;2*rMsC8BH#+qsACZ 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 literal 0 HcmV?d00001 diff --git a/data/sans.12.kerning.bin b/data/sans.12.kerning.bin new file mode 100644 index 0000000000000000000000000000000000000000..843d7967ac4328ffd767cc009d834a737335d157 GIT binary patch literal 1164 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 a695;&p}cp4gk+j*f#9yA>V-4q)&By%=fGe9 literal 0 HcmV?d00001 diff --git a/data/sans.16.kerning.bin b/data/sans.16.kerning.bin new file mode 100644 index 0000000000000000000000000000000000000000..be40c26c1beb062e6f4c12f9262cd771e53e09c8 GIT binary patch literal 1644 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 G*Z(g_j}~hH literal 0 HcmV?d00001 diff --git a/data/sans.8.kerning.bin b/data/sans.8.kerning.bin new file mode 100644 index 0000000000000000000000000000000000000000..f95aff59e4fadc03944f5d21546de438b897a820 GIT binary patch literal 336 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 Ng-T(F%-l=W{{=PU(~STC literal 0 HcmV?d00001 diff --git a/ftbrony.png b/ftbrony.png new file mode 100644 index 0000000000000000000000000000000000000000..0081995706ad38f4a8d76992e1f902366929aa51 GIT binary patch literal 6575 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 literal 0 HcmV?d00001 diff --git a/include/console.h b/include/console.h new file mode 100644 index 0000000..3abc432 --- /dev/null +++ b/include/console.h @@ -0,0 +1,12 @@ +#pragma once + +void console_init(void); +void console_exit(void); + +__attribute__((format(printf,1,2))) +void console_set_status(const char *fmt, ...); + +__attribute__((format(printf,1,2))) +void console_print(const char *fmt, ...); + +void console_render(void); diff --git a/include/debug.h b/include/debug.h new file mode 100644 index 0000000..0708908 --- /dev/null +++ b/include/debug.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#ifdef _3DS +#include <3ds.h> +#endif + +/*! print debug message + * + * @param[in] fmt format string + * @param[in] ap varargs list + * + * @returns number of characters written + */ +static inline int +vdebug(const char *fmt, + va_list ap) +{ +#ifdef _3DS + int rc; + char buffer[256]; + + memset(buffer, 0, sizeof(buffer)); + + /* print to buffer */ + rc = vsnprintf(buffer, sizeof(buffer)-1, fmt, ap); + + /* call debug service with buffer */ + svcOutputDebugString(buffer, rc < sizeof(buffer) ? rc : sizeof(buffer)); + return rc; +#else + /* just print to stdout */ + return vprintf(fmt, ap); +#endif +} + +__attribute__((format(printf,1,2))) +/*! print debug message + * + * @param[in] fmt format string + * @param[in] ... format arguments + * + * @returns number of characters written + */ +static inline int +debug(const char *fmt, ...) +{ + int rc; + va_list ap; + va_start(ap, fmt); + rc = vdebug(fmt, ap); + va_end(ap); + return rc; +} diff --git a/include/ftp.h b/include/ftp.h new file mode 100644 index 0000000..ac402ba --- /dev/null +++ b/include/ftp.h @@ -0,0 +1,5 @@ +#pragma once + +int ftp_init(void); +int ftp_loop(void); +void ftp_exit(void); diff --git a/source/console.c b/source/console.c new file mode 100644 index 0000000..34870d3 --- /dev/null +++ b/source/console.c @@ -0,0 +1,735 @@ +#include "console.h" +#include +#include +#include +#ifdef _3DS +#include <3ds.h> +#endif +#include "debug.h" + +#ifdef _3DS +#include "sans_8_kerning_bin.h" +#include "sans_8_render_bin.h" +#include "sans_10_kerning_bin.h" +#include "sans_10_render_bin.h" +#include "sans_12_kerning_bin.h" +#include "sans_12_render_bin.h" +#include "sans_14_kerning_bin.h" +#include "sans_14_render_bin.h" +#include "sans_16_kerning_bin.h" +#include "sans_16_render_bin.h" + +/* TODO: add support for non-ASCII characters */ + +/*! rendering information */ +typedef struct +{ + int c; /*!< character */ + int y_off; /*!< vertical offset */ + int width; /*!< width */ + int height; /*!< height */ + int x_adv; /*!< horizontal advance */ + u8 data[]; /*!< width*height bitmap */ +} render_info_t; + +/*! kerning information */ +typedef struct +{ + int prev; /*!< previous character */ + int next; /*!< next character */ + int x_off; /*!< horizontal adjustment */ +} kerning_info_t; + +/*! font data */ +typedef struct +{ + const char *name; /*!< font name */ + render_info_t **render_info; /*!< render information list */ + kerning_info_t *kerning_info; /*!< kerning information list */ + size_t num_render_info; /*!< number of render information nodes */ + size_t num_kerning_info; /*!< number of kerning information nodes */ + int pt; /*!< font size */ +} font_t; + +/*! font information */ +typedef struct +{ + const char *name; /*!< font name */ + int pt; /*!< font size */ + const u8 *render_data; /*!< render data */ + const u8 *kerning_data; /*!< kerning data */ + const u32 *render_data_size; /*!< render data size */ + const u32 *kerning_data_size; /*!< kerning data size */ +} font_info_t; + +/*! font descriptors */ +static font_info_t font_info[] = +{ +#define FONT_INFO(name, pt) \ + { #name, pt, name##_##pt##_render_bin, name##_##pt##_kerning_bin, \ + &name##_##pt##_render_bin_size, &name##_##pt##_kerning_bin_size, } + FONT_INFO(sans, 8), + FONT_INFO(sans, 10), + FONT_INFO(sans, 12), + FONT_INFO(sans, 14), + FONT_INFO(sans, 16), +}; +/*! number of font descriptors */ +static const size_t num_font_info = sizeof(font_info)/sizeof(font_info[0]); + +/*! find next render info + * + * @param[in] info current render info + * + * @returns next render info + */ +static render_info_t* +next_render_info(render_info_t *info) +{ + char *ptr = (char*)info; + ptr += sizeof(*info) + info->width*info->height; + ptr = (char*)(((int)ptr + sizeof(int)-1) & ~(sizeof(int)-1)); + + return (render_info_t*)ptr; +} + +/*! free font info + * + * @param[in] font + */ +static void +free_font(font_t *font) +{ + free(font->render_info); + free(font); +} + +/*! load font info + * + * @param[in] name + * @param[in] pt + * @param[in] render_data + * @param[in] render_data_size + * @param[in] kerning data + * @param[in] kerning_data_size + * + * @returns font info + */ +static font_t* +load_font(const char *name, + int pt, + const u8 *render_data, + size_t render_data_size, + const u8 *kerning_data, + size_t kerning_data_size) +{ + size_t i; + render_info_t *rinfo; + font_t *font; + + /* allocate new font info */ + font = (font_t*)calloc(1, sizeof(font_t)); + if(font != NULL) + { + /* count number of render entries */ + rinfo = (render_info_t*)render_data; + while((u8*)rinfo < render_data + render_data_size) + { + ++font->num_render_info; + rinfo = next_render_info(rinfo); + } + + /* allocate array of render info pointers */ + font->render_info = (render_info_t**)calloc(font->num_render_info, sizeof(render_info_t)); + if(font->render_info != NULL) + { + /* fill in the pointer list */ + rinfo = (render_info_t*)render_data; + i = 0; + while((u8*)rinfo < render_data + render_data_size) + { + font->render_info[i++] = rinfo; + rinfo = next_render_info(rinfo); + } + + /* fill in the kerning info */ + font->kerning_info = (kerning_info_t*)kerning_data; + font->num_kerning_info = kerning_data_size / sizeof(kerning_info_t); + + /* set font size and name */ + font->pt = pt; + font->name = name; + } + else + { + /* failed to allocate render info list */ + free_font(font); + font = NULL; + } + } + + return font; +} + +/*! list of font info entries */ +static font_t **fonts; +/*! number of font info entries */ +static size_t num_fonts = 0; + +/*! compare two fonts + * + * @param[in] p1 left side of comparison (font_t**) + * @param[in] p2 right side of comparison (font_t**) + * + * @returns <0 if p1 < p2 + * @returns 0 if p1 == p2 + * @returns >0 if p1 > p2 + */ +static int +font_cmp(const void *p1, + const void *p2) +{ + /* interpret parameters */ + font_t *f1 = *(font_t**)p1; + font_t *f2 = *(font_t**)p2; + + /* major key is font name */ + int rc = strcmp(f1->name, f2->name); + if(rc != 0) + return rc; + + /* minor key is font size */ + if(f1->pt < f2->pt) + return -1; + if(f1->pt > f2->pt) + return 1; + return 0; +} + +/*! search for a font by name and size + * + * @param[in] name font name + * @param[in] pt font size + * + * @returns matching font + */ +static font_t* +find_font(const char *name, + int pt) +{ + /* create a key to search for */ + font_t key, *keyptr; + key.name = name; + key.pt = pt; + keyptr = &key; + + /* search for the key */ + void *font = bsearch(&keyptr, fonts, num_fonts, sizeof(font_t*), font_cmp); + if(font == NULL) + return NULL; + + /* found it */ + return *(font_t**)font; +} + +/*! initialize console subsystem */ +void +console_init(void) +{ + size_t i; + + /* allocate font list */ + fonts = (font_t**)calloc(num_font_info, sizeof(font_t*)); + if(fonts == NULL) + return; + + /* load fonts */ + for(i = 0; i < num_font_info; ++i) + { + font_info_t *info = &font_info[i]; + fonts[num_fonts] = load_font(info->name, info->pt, + info->render_data, + *info->render_data_size, + info->kerning_data, + *info->kerning_data_size); + if(fonts[num_fonts] != NULL) + ++num_fonts; + } + + /* sort the list for bsearch later */ + qsort(fonts, num_fonts, sizeof(font_t*), font_cmp); +} + +/*! deinitialize console subsystem */ +void +console_exit(void) +{ + int i; + + /* free the font info */ + for(i = 0; i < num_fonts; ++i) + free_font(fonts[i]); + + /* free the font info list */ + free(fonts); + fonts = NULL; +} + +/*! status bar contents */ +static char status[64]; +/*! console buffer */ +static char buffer[8192]; +/*! pointer to end of buffer */ +static char *buffer_end = buffer + sizeof(buffer); +/*! pointer to end of console contents */ +static char *end = buffer; + +/*! count lines in console contents */ +static size_t +count_lines(void) +{ + size_t lines = 0; + char *p = buffer; + + /* search for each newline character */ + while(p < end && (p = strchr(p, '\n')) != NULL) + { + ++lines; + ++p; + } + + return lines; +} + +/*! remove lines that have "scrolled" off screen */ +static void +reduce_lines(void) +{ + int lines = count_lines(); + char *p = buffer; + + /* we can fit 18 lines on the screen */ + /* TODO make based on pt size */ + while(lines > 18) + { + p = strchr(p, '\n'); + ++p; + --lines; + } + + /* move the new beginning to where it needs to be */ + ptrdiff_t distance = p - buffer; + memmove(buffer, buffer+distance, end - p); + end -= distance; + *end = 0; +} + +/*! set status bar contents + * + * @param[in] fmt format string + * @param[in] ... format arguments + */ +void +console_set_status(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + memset(status, 0, sizeof(status)); + vsnprintf(status, sizeof(status)-1, fmt, ap); + va_end(ap); +} + +/*! add text to the console + * + * @param[in] fmt format string + * @param[in] ... format arguments + */ +void +console_print(const char *fmt, ...) +{ + int rc; + va_list ap; + + /* append to the end of the console buffer */ + va_start(ap, fmt); + rc = vsnprintf(end, buffer_end - end - 1, fmt, ap); + va_end(ap); + + /* null terminate buffer */ + end += rc; + if(end >= buffer_end) + end = buffer_end - 1; + *end = 0; + + /* scroll */ + reduce_lines(); +} + +/*! compare render information + * + * @param[in] p1 left side of comparison (render_info_t**) + * @param[in] p2 right side of comparison (render_info_t**) + * + * @returns <0 if p1 < p2 + * @returns 0 if p1 == p2 + * @returns >0 if p1 > p2 + */ +static int +render_info_cmp(const void *p1, + const void *p2) +{ + /* interpret parameters */ + render_info_t *r1 = *(render_info_t**)p1; + render_info_t *r2 = *(render_info_t**)p2; + + /* ordered by character */ + if(r1->c < r2->c) + return -1; + else if(r1->c > r2->c) + return 1; + return 0; +} + +/*! search for render info by character + * + * @param[in] font font info + * @param[in] char character + * + * @returns matching render info + */ +static render_info_t* +find_render_info(font_t *font, + char c) +{ + /* create a key to search for */ + render_info_t key, *keyptr; + key.c = c; + keyptr = &key; + + /* search for the key */ + void *info = bsearch(&keyptr, font->render_info, font->num_render_info, + sizeof(render_info_t*), render_info_cmp); + if(info == NULL) + return NULL; + + /* found it */ + return *(render_info_t**)info; +} + +/*! compare kerning information + * + * @param[in] p1 left side of comparison (kerning_info_t*) + * @param[in] p2 right side of comparison (kerning_info_t*) + * + * @returns <0 if p1 < p2 + * @returns 0 if p1 == p2 + * @returns >0 if p1 > p2 + */ +static int +kerning_info_cmp(const void *p1, + const void *p2) +{ + /* interpret parameters */ + kerning_info_t *k1 = (kerning_info_t*)p1; + kerning_info_t *k2 = (kerning_info_t*)p2; + + /* major key is prev */ + if(k1->prev < k2->prev) + return -1; + if(k1->prev > k2->prev) + return 1; + + /* minor key is next */ + if(k1->next < k2->next) + return -1; + if(k1->next > k2->next) + return 1; + + return 0; +} + +/*! search for kerning info by character pair + * + * @param[in] font font info + * @param[in] prev prev character + * @param[in] next next character + * + * @returns matching render info + */ +static kerning_info_t* +find_kerning_info(font_t *font, + char prev, + char next) +{ + /* create a key to search for */ + kerning_info_t key; + key.prev = prev; + key.next = next; + + /* search for the key */ + void *info = bsearch(&key, font->kerning_info, font->num_kerning_info, + sizeof(kerning_info_t), kerning_info_cmp); + if(info == NULL) + return NULL; + + /* found it */ + return (kerning_info_t*)info; +} + +/*! clear framebuffer + * + * @param[in] screen screen to clear + * @param[in] side which side on the stereoscopic display + * @param[in] rgbColor clear color + */ +static void +clear_screen(gfxScreen_t screen, + gfx3dSide_t side, + u8 rgbColor[3]) +{ + /* get the framebuffer information */ + u16 fbWidth, fbHeight; + u8 *fb = gfxGetFramebuffer(screen, side, &fbWidth, &fbHeight); + + /* fill the framebuffer with the clear color */ + int i; + for(i = 0; i < fbWidth*fbHeight; ++i) + { + *(fb++) = rgbColor[2]; + *(fb++) = rgbColor[1]; + *(fb++) = rgbColor[0]; + } +} + +/*! draw a quad + * + * @param[in] screen screen to draw to + * @param[in] side which side on the stereoscopic display + * @param[in] data quad data + * @param[in] x quad x position + * @param[in] y quad y position + * @param[in] w quad width + * @param[in] h quad height + * + * @note this quad data is 8-bit alpha-only + * @note uses framebuffer native coordinates + */ +static void +draw_quad(gfxScreen_t screen, + gfx3dSide_t side, + const u8 *data, + int x, + int y, + int w, + int h) +{ + int i, j; + int index = 0; + int stride = w; + + /* get the framebuffer information */ + u16 width, height; + u8 *fb = gfxGetFramebuffer(screen, side, &width, &height); + + /* this quad is totally offscreen; don't draw */ + if(x > width || y > height) + return; + + /* this quad is totally offscreen; don't draw */ + if(x + w < 0 || y + h < 0) + return; + + /* adjust parameters for partially visible quad */ + if(x < 0) + { + index -= x; + w += x; + x = 0; + } + + /* adjust parameters for partially visible quad */ + if(y < 0) + { + index -= y*stride; + h += y; + y = 0; + } + + /* adjust parameters for partially visible quad */ + if(x + w > width) + w = width - x; + + /* adjust parameters for partially visible quad */ + if(y + h > height) + h = height - y; + + /* move framebuffer pointer to quad start position */ + fb += (y*width + x)*3; + + /* fill in data */ + for(j = 0; j < h; ++j) + { + for(i = 0; i < w; ++i) + { + /* alpha blending; assuming color is white */ + int v = data[index]; + fb[0] = fb[0]*(0xFF-v)/0xFF + v; + fb[1] = fb[1]*(0xFF-v)/0xFF + v; + fb[2] = fb[2]*(0xFF-v)/0xFF + v; + + ++index; + fb += 3; + } + + index += (stride-w); + fb += (width-w)*3; + } +} + +/*! draw text to framebuffer + * + * @param[in] screen screen to draw to + * @param[in] side which side on the stereoscopic display + * @param[in] font font to use when rendering + * @param[in] data quad data + * @param[in] x quad x position + * @param[in] y quad y position + * + * @note uses intuitive coordinates + */ +static void +draw_text(gfxScreen_t screen, + gfx3dSide_t side, + font_t *font, + const char *data, + int x, + int y) +{ + render_info_t *rinfo; + kerning_info_t *kinfo; + const char *p; + int xoff = x, yoff = y; + char prev = 0; + + /* draw each character */ + for(p = data; *p != 0; ++p) + { + /* newline; move down a line and all the way left */ + if(*p == '\n') + { + xoff = x; + yoff += font->pt + font->pt/2; + prev = 0; + continue; + } + + /* look up the render info for this character */ + rinfo = find_render_info(font, *p); + + /* couldn't find it; just ignore it */ + if(rinfo == NULL) + continue; + + /* find kerning data */ + kinfo = NULL; + if(prev != 0) + kinfo = find_kerning_info(font, prev, *p); + + /* adjust for kerning */ + if(kinfo != NULL) + xoff += kinfo->x_off >> 6; + + /* save this character for next kerning lookup */ + prev = *p; + + /* render character */ + if(rinfo->width != 0 && rinfo->height != 0) + { + int x, y; + + /* get framebuffer info */ + u16 width, height; + gfxGetFramebuffer(screen, side, &width, &height); + + /* transform intuitive coordinates to framebuffer-native */ + x = width - yoff - font->pt - 2 - (rinfo->height - rinfo->y_off); + y = xoff; + + /* draw character */ + draw_quad(screen, side, rinfo->data, x, y, + rinfo->height, rinfo->width); + } + + /* advance to next character coordinate */ + xoff += rinfo->x_adv >> 6; + } +} + +/*! draw console to screen */ +void +console_render(void) +{ + font_t *font; + + /* clear all screens */ + u8 bluish[] = { 0, 0, 127 }; + clear_screen(GFX_TOP, GFX_LEFT, bluish); + clear_screen(GFX_BOTTOM, GFX_LEFT, bluish); + + /* look up font for status bar and draw status bar */ + font = find_font("sans", 10); + if(font != NULL) + draw_text(GFX_TOP, GFX_LEFT, font, status, 4, 4); + else + debug("%s: couldn't find 'sans 10pt'\n", __func__); + + /* look up font for console and draw console */ + font = find_font("sans", 8); + if(font != NULL) + draw_text(GFX_TOP, GFX_LEFT, font, buffer, 4, 20); + else + debug("%s: couldn't find 'sans 8pt'\n", __func__); + + /* flush framebuffer */ + gfxFlushBuffers(); + gspWaitForVBlank(); + gfxSwapBuffers(); +} +#else + +/* this is a lot easier when you have a real console */ + +void +console_init(void) +{ +} + +void +console_exit(void) +{ +} + +void +console_set_status(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + fputc('\n', stdout); +} + +void +console_print(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); +} + +void console_render(void) +{ +} +#endif diff --git a/source/ftp.c b/source/ftp.c new file mode 100644 index 0000000..1aab602 --- /dev/null +++ b/source/ftp.c @@ -0,0 +1,2136 @@ +#include "ftp.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _3DS +#include <3ds.h> +#else +#include +#include +#include +#endif +#include "console.h" + +#ifdef _3DS +/* not sure what's correct yet */ +#undef POLLIN +#define POLLIN 0x001 // looks correct +#undef POLLOUT +#define POLLOUT 0x010 // looks reasonable +#undef POLLERR +#define POLLERR 0x000 // ??? +#undef POLLHUP +#define POLLHUP 0x000 // ??? +#else +#define SOC_GetErrno() errno +#define closesocket(x) close(x) +#endif +#define POLL_UNKNOWN (~(POLLIN|POLLOUT)) + +#define SOC_ALIGN 0x1000 +#define SOC_BUFFERSIZE 0x100000 +#define LISTEN_PORT 5000 +#ifdef _3DS +#define DATA_PORT (LISTEN_PORT+1) +#else +#define DATA_PORT 0 /* ephemeral port */ +#endif + +typedef struct ftp_session ftp_session_t; + +#define FTP_DECLARE(x) static int x(ftp_session_t *session, const char *args) +FTP_DECLARE(ALLO); +FTP_DECLARE(APPE); +FTP_DECLARE(CDUP); +FTP_DECLARE(CWD); +FTP_DECLARE(DELE); +FTP_DECLARE(FEAT); +FTP_DECLARE(LIST); +FTP_DECLARE(MKD); +FTP_DECLARE(MODE); +FTP_DECLARE(NLST); +FTP_DECLARE(NOOP); +FTP_DECLARE(PASS); +FTP_DECLARE(PASV); +FTP_DECLARE(PORT); +FTP_DECLARE(PWD); +FTP_DECLARE(QUIT); +FTP_DECLARE(REST); +FTP_DECLARE(RETR); +FTP_DECLARE(RMD); +FTP_DECLARE(RNFR); +FTP_DECLARE(RNTO); +FTP_DECLARE(STOR); +FTP_DECLARE(STOU); +FTP_DECLARE(STRU); +FTP_DECLARE(SYST); +FTP_DECLARE(TYPE); +FTP_DECLARE(USER); + +/*! session state */ +typedef enum +{ + COMMAND_STATE, /*!< waiting for a command */ + DATA_CONNECT_STATE, /*!< waiting for connection after PASV command */ + DATA_TRANSFER_STATE, /*!< data transfer in progress */ +} session_state_t; + +/*! ftp session */ +struct ftp_session +{ + char cwd[4096]; /*!< current working directory */ + struct sockaddr_in peer_addr; /*!< peer address for data connection */ + struct sockaddr_in pasv_addr; /*!< listen address for PASV connection */ + int cmd_fd; /*!< socket for command connection */ + int pasv_fd; /*!< listen socket for PASV */ + int data_fd; /*!< socket for data transfer */ +/*! data transfers in binary mode */ +#define SESSION_BINARY (1 << 0) +/*! have pasv_addr ready for data transfer command */ +#define SESSION_PASV (1 << 1) +/*! have peer_addr ready for data transfer command */ +#define SESSION_PORT (1 << 2) +/*! data transfer in source mode */ +#define SESSION_RECV (1 << 3) +/*! data transfer in sink mode */ +#define SESSION_SEND (1 << 4) +/*! last command was RNFR and buffer contains path */ +#define SESSION_RENAME (1 << 5) + int flags; /*!< session flags */ + session_state_t state; /*!< session state */ + ftp_session_t *next; /*!< link to next session */ + ftp_session_t *prev; /*!< link to prev session */ + + void (*transfer)(ftp_session_t*); /*! data transfer callback */ + char buffer[1024]; /*! persistent data between callbacks */ + size_t bufferpos; /*! persistent buffer position between callbacks */ + size_t buffersize; /*! persistent buffer size between callbacks */ + uint64_t filepos; /*! persistent file position between callbacks */ + uint64_t filesize; /*! persistent file size between callbacks */ +#ifdef _3DS + Handle fd; /*! persistent handle between callbacks */ +#else + union + { + DIR *dp; /*! persistent open directory pointer between callbacks */ + int fd; /*! persistent open file descriptor between callbacks */ + }; +#endif +}; + +/*! ftp command descriptor */ +typedef struct ftp_command +{ + const char *name; /*!< command name */ + int (*handler)(ftp_session_t*, const char*); /*!< command callback */ +} ftp_command_t; + +static ftp_command_t ftp_commands[] = +{ +#define FTP_COMMAND(x) { #x, x, } +#define FTP_ALIAS(x,y) { #x, y, } + FTP_COMMAND(ALLO), + FTP_COMMAND(APPE), + FTP_COMMAND(CDUP), + FTP_COMMAND(CWD), + FTP_COMMAND(DELE), + FTP_COMMAND(FEAT), + FTP_COMMAND(LIST), + FTP_COMMAND(MKD), + FTP_COMMAND(MODE), + FTP_COMMAND(NLST), + FTP_COMMAND(NOOP), + FTP_COMMAND(PASS), + FTP_COMMAND(PASV), + FTP_COMMAND(PORT), + FTP_COMMAND(PWD), + FTP_COMMAND(QUIT), + FTP_COMMAND(REST), + FTP_COMMAND(RETR), + FTP_COMMAND(RMD), + FTP_COMMAND(RNFR), + FTP_COMMAND(RNTO), + FTP_COMMAND(STOR), + FTP_COMMAND(STOU), + FTP_COMMAND(STRU), + FTP_COMMAND(SYST), + FTP_COMMAND(TYPE), + FTP_COMMAND(USER), + FTP_ALIAS(XCUP, CDUP), + FTP_ALIAS(XMKD, MKD), + FTP_ALIAS(XPWD, PWD), + FTP_ALIAS(XRMD, RMD), +}; +/*! number of ftp commands */ +static const size_t num_ftp_commands = sizeof(ftp_commands)/sizeof(ftp_commands[0]); + +/*! compare ftp command descriptors + * + * @param[in] p1 left side of comparison (ftp_command_t*) + * @param[in] p2 right side of comparison (ftp_command_t*) + * + * @returns <0 if p1 < p2 + * @returns 0 if p1 == p2 + * @returns >0 if p1 > p2 + */ +static int +ftp_command_cmp(const void *p1, + const void *p2) +{ + ftp_command_t *c1 = (ftp_command_t*)p1; + ftp_command_t *c2 = (ftp_command_t*)p2; + + /* ordered by command name */ + return strcasecmp(c1->name, c2->name); +} + +#ifdef _3DS +/*! SOC service buffer */ +static u32 *SOC_buffer = NULL; + +/*! SDMC archive */ +static FS_archive sdmcArchive = +{ + .id = ARCH_SDMC, + .lowPath = + { + .type = PATH_EMPTY, + .size = 1, + .data = (u8*)"", + }, +}; + +/*! convert 3DS dirent name to ASCII + * + * TODO: add support for non-ASCII characters + * + * @param[in] dst output buffer + * @param[in] src input buffer + */ +static void +convert_name(char *dst, + const u16 *src) +{ + while(*src) + *dst++ = *src++; + *dst = 0; +} +#endif + +/*! server listen address */ +static struct sockaddr_in serv_addr; +/*! listen file descriptor */ +static int listenfd = -1; +/*! list of ftp sessions */ +ftp_session_t *sessions = NULL; + +/*! close a socket + * + * @param[in] fd socket to close + * @param[in] connected whether this socket is connected + */ +static void +ftp_closesocket(int fd, int connected) +{ + int rc; + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + + if(connected) + { + /* get peer address and print */ + rc = getpeername(fd, (struct sockaddr*)&addr, &addrlen); + if(rc != 0) + { + console_print("getpeername: %s\n", strerror(SOC_GetErrno())); + console_print("closing connection to fd=%d\n", fd); + } + else + console_print("closing connection to %s:%u\n", + inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); + + /* shutdown connection */ + rc = shutdown(fd, SHUT_RDWR); + if(rc != 0) + console_print("shutdown: %s\n", strerror(SOC_GetErrno())); + } + + /* close socket */ + rc = closesocket(fd); + if(rc != 0) + console_print("closesocket: %s\n", strerror(SOC_GetErrno())); +} + +/*! close command socket on ftp session + * + * @param[in] session ftp session + */ +static void +ftp_session_close_cmd(ftp_session_t *session) +{ + /* close command socket */ + ftp_closesocket(session->cmd_fd, 1); + session->cmd_fd = -1; +} + +/*! close listen socket on ftp session + * + * @param[in] session ftp session + */ +static void +ftp_session_close_pasv(ftp_session_t *session) +{ + console_print("stop listening on %s:%u\n", + inet_ntoa(session->pasv_addr.sin_addr), + ntohs(session->pasv_addr.sin_port)); + + /* close pasv socket */ + ftp_closesocket(session->pasv_fd, 0); + session->pasv_fd = -1; + + /* allocate new port for next PASV */ +#ifdef _3DS + /* just increment the port */ + /* TODO: use global port variable so separate sessions don't collide */ + session->pasv_addr.sin_port = htons(ntohs(session->pasv_addr.sin_port)+1); +#else + /* get an ephemeral port */ + session->pasv_addr.sin_port = htons(0); +#endif +} + +/*! close data socket on ftp session + * + * @param[in] session ftp session */ +static void +ftp_session_close_data(ftp_session_t *session) +{ + /* close data connection */ + ftp_closesocket(session->data_fd, 1); + session->data_fd = -1; + + /* clear send/recv flags */ + session->flags &= ~(SESSION_RECV|SESSION_SEND); +} + +/*! close open file for ftp session + * + * @param[in] session ftp session + */ +static void +ftp_session_close_file(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + + ret = FSFILE_Close(session->fd); + if(ret != 0) + console_print("FSFILE_Close: 0x%08X\n", (unsigned int)ret); + session->fd = -1; +#else + int rc; + + rc = close(session->fd); + if(rc != 0) + console_print("close: %s\n", strerror(errno)); + session->fd = -1; +#endif +} + +/*! open file for reading for ftp session + * + * @param[in] session ftp session + * + * @returns -1 for error + */ +static int +ftp_session_open_file_read(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + u64 size; + + /* open file in read mode */ + ret = FSUSER_OpenFile(NULL, &session->fd, sdmcArchive, + FS_makePath(PATH_CHAR, session->buffer), + FS_OPEN_READ, FS_ATTRIBUTE_NONE); + if(ret != 0) + { + console_print("FSUSER_OpenFile: 0x%08X\n", (unsigned int)ret); + return -1; + } + + /* get the file size */ + ret = FSFILE_GetSize(session->fd, &size); + if(ret != 0) + { + console_print("FSFILE_GetSize: 0x%08X\n", (unsigned int)ret); + ftp_session_close_file(session); + return -1; + } + session->filesize = size; +#else + int rc; + struct stat st; + + /* open file in read mode */ + session->fd = open(session->buffer, O_RDONLY); + if(session->fd < 0) + { + console_print("open '%s': %s\n", session->buffer, strerror(errno)); + return -1; + } + + /* get the file size */ + rc = fstat(session->fd, &st); + if(rc != 0) + { + console_print("fstat '%s': %s\n", session->buffer, strerror(errno)); + ftp_session_close_file(session); + return -1; + } + session->filesize = st.st_size; +#endif + + /* reset file position */ + /* TODO: support REST command */ + session->filepos = 0; + + return 0; +} + +/*! read from an open file for ftp session + * + * @param[in] session ftp session + * + * @returns bytes read + */ +static ssize_t +ftp_session_read_file(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + u32 bytes; + + /* read file at current position */ + ret = FSFILE_Read(session->fd, &bytes, session->filepos, + session->buffer, sizeof(session->buffer)); + if(ret != 0) + { + console_print("FSFILE_Read: 0x%08X\n", (unsigned int)ret); + return -1; + } + + /* adjust file position */ + session->filepos += bytes; + + return bytes; +#else + ssize_t rc; + + /* read file at current position */ + /* TODO: maybe use pread? */ + rc = read(session->fd, session->buffer, sizeof(session->buffer)); + if(rc < 0) + { + console_print("read: %s\n", strerror(errno)); + return -1; + } + + /* adjust file position */ + session->filepos += rc; + + return rc; +#endif +} + +/*! open file for writing for ftp session + * + * @param[in] session ftp session + * + * @returns -1 for error + * + * @note truncates file + */ +static int +ftp_session_open_file_write(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + + /* open file in write and create mode */ + ret = FSUSER_OpenFile(NULL, &session->fd, sdmcArchive, + FS_makePath(PATH_CHAR, session->buffer), + FS_OPEN_WRITE|FS_OPEN_CREATE, FS_ATTRIBUTE_NONE); + if(ret != 0) + { + console_print("FSUSER_OpenFile: 0x%08X\n", (unsigned int)ret); + return -1; + } + + /* truncate file */ + ret = FSFILE_SetSize(session->fd, 0); + if(ret != 0) + { + console_print("FSFILE_SetSize: 0x%08X\n", (unsigned int)ret); + ftp_session_close_file(session); + } +#else + /* open file in write and create mode with truncation */ + session->fd = open(session->buffer, O_WRONLY|O_CREAT|O_TRUNC, 0644); + if(session->fd < 0) + { + console_print("open '%s': %s\n", session->buffer, strerror(errno)); + return -1; + } +#endif + + /* reset file position */ + /* TODO: support REST command */ + session->filepos = 0; + + return 0; +} + +/*! write to an open file for ftp session + * + * @param[in] session ftp session + * + * @returns bytes written + */ +static ssize_t +ftp_session_write_file(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + u32 bytes; + + /* write to file at current position */ + ret = FSFILE_Write(session->fd, &bytes, session->filepos, + session->buffer + session->bufferpos, + session->buffersize - session->bufferpos, + FS_WRITE_FLUSH); + if(ret != 0) + { + console_print("FSFILE_Write: 0x%08X\n", (unsigned int)ret); + return -1; + } + else if(ret == 0) + console_print("FSFILE_Write: wrote 0 bytes\n"); + + /* adjust file position */ + session->filepos += bytes; + + return bytes; +#else + ssize_t rc; + + /* write to file at current position */ + /* TODO: maybe use writev? */ + rc = write(session->fd, session->buffer + session->bufferpos, + session->buffersize - session->bufferpos); + if(rc < 0) + { + console_print("write: %s\n", strerror(errno)); + return -1; + } + else if(rc == 0) + console_print("write: wrote 0 bytes\n"); + + /* adjust file position */ + session->filepos += rc; + + return rc; +#endif +} + +/*! close current working directory for ftp session + * + * @param[in] session ftp session + */ +static void +ftp_session_close_cwd(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + + /* close open directory handle */ + ret = FSDIR_Close(session->fd); + if(ret != 0) + console_print("FSDIR_Close: 0x%08X\n", (unsigned int)ret); + session->fd = -1; +#else + int rc; + + /* close open directory pointer */ + rc = closedir(session->dp); + if(rc != 0) + console_print("closedir: %s\n", strerror(errno)); + session->dp = NULL; +#endif +} + +/*! open current working directory for ftp session + * + * @param[in] session ftp session + * + * @return -1 for failure + */ +static int +ftp_session_open_cwd(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; + + /* open current working directory */ + ret = FSUSER_OpenDirectory(NULL, &session->fd, sdmcArchive, + FS_makePath(PATH_CHAR, session->cwd)); + if(ret != 0) + { + console_print("FSUSER_OpenDirectory: 0x%08X\n", (unsigned int)ret); + return -1; + } +#else + /* open current working directory */ + session->dp = opendir(session->cwd); + if(session->dp == NULL) + { + console_print("opendir '%s': %s\n", session->cwd, strerror(errno)); + return -1; + } +#endif + + return 0; +} + +/*! set state for ftp session + * + * @param[in] session ftp session + * @param[in] state state to set + */ +static void +ftp_session_set_state(ftp_session_t *session, + session_state_t state) +{ + session->state = state; + + switch(state) + { + case COMMAND_STATE: + /* close pasv and data sockets */ + if(session->pasv_fd >= 0) + ftp_session_close_pasv(session); + if(session->data_fd >= 0) + ftp_session_close_data(session); + break; + + case DATA_CONNECT_STATE: + /* close data socket; we are listening for a new one */ + if(session->data_fd >= 0) + ftp_session_close_data(session); + break; + + case DATA_TRANSFER_STATE: + /* close pasv socket; we are connecting for a new one */ + if(session->pasv_fd >= 0) + ftp_session_close_pasv(session); + } +} + +__attribute__((format(printf,3,4))) +/*! send ftp response to ftp session's peer + * + * @param[in] session ftp session + * @param[in] code response code + * @param[in] fmt format string + * @param[in] ... format arguments + * + * returns bytes send to peer + */ +static ssize_t +ftp_send_response(ftp_session_t *session, + int code, + const char *fmt, ...) +{ + static char buffer[1024]; + ssize_t rc, to_send; + va_list ap; + + /* print response code and message to buffer */ + va_start(ap, fmt); + rc = sprintf(buffer, "%d ", code); + rc += vsnprintf(buffer+rc, sizeof(buffer)-rc, fmt, ap); + va_end(ap); + + if(rc >= sizeof(buffer)) + { + /* couldn't fit message; just send code */ + console_print("%s: buffersize too small\n", __func__); + rc = sprintf(buffer, "%d\r\n", code); + } + + /* send response */ + to_send = rc; + console_print("%s", buffer); + rc = send(session->cmd_fd, buffer, to_send, 0); + if(rc < 0) + console_print("send: %s\n", strerror(SOC_GetErrno())); + else if(rc != to_send) + console_print("only sent %u/%u bytes\n", + (unsigned int)rc, (unsigned int)to_send); + + return rc; +} + +/*! destroy ftp session + * + * @param[in] session ftp session + * + * @returns next session in list + */ +static ftp_session_t* +ftp_session_destroy(ftp_session_t *session) +{ + ftp_session_t *next = session->next; + + /* close all sockets */ + if(session->cmd_fd >= 0) + ftp_session_close_cmd(session); + if(session->pasv_fd >= 0) + ftp_session_close_pasv(session); + if(session->data_fd >= 0) + ftp_session_close_data(session); + + /* unlink from sessions list */ + if(session->next) + session->next->prev = session->prev; + if(session == sessions) + sessions = session->next; + else + { + session->prev->next = session->next; + if(session == sessions->prev) + sessions->prev = session->prev; + } + + /* deallocate */ + free(session); + + return next; +} + +/*! allocate new ftp session + * + * @param[in] listen_fd socket to accept connection from + */ +static void +ftp_session_new(int listen_fd) +{ + ssize_t rc; + int new_fd; + ftp_session_t *session; + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + + /* accept connection */ + new_fd = accept(listen_fd, (struct sockaddr*)&addr, &addrlen); + if(new_fd < 0) + { + console_print("accept: %s\n", strerror(SOC_GetErrno())); + return; + } + + console_print("accepted connection from %s:%u\n", + inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); + + /* allocate a new session */ + session = (ftp_session_t*)malloc(sizeof(ftp_session_t)); + if(session == NULL) + { + console_print("failed to allocate session\n"); + ftp_closesocket(new_fd, 1); + return; + } + + /* initialize session */ + memset(session->cwd, 0, sizeof(session->cwd)); + strcpy(session->cwd, "/"); + session->peer_addr.sin_addr.s_addr = INADDR_ANY; + session->cmd_fd = new_fd; + session->pasv_fd = -1; + session->data_fd = -1; + session->flags = 0; + session->state = COMMAND_STATE; + session->next = NULL; + session->prev = NULL; + session->transfer = NULL; + + /* link to the sessions list */ + if(sessions == NULL) + { + sessions = session; + session->prev = session; + } + else + { + sessions->prev->next = session; + session->prev = sessions->prev; + sessions->prev = session; + } + + /* copy socket address to pasv address */ + addrlen = sizeof(session->pasv_addr); + rc = getsockname(new_fd, (struct sockaddr*)&session->pasv_addr, &addrlen); + if(rc != 0) + { + console_print("getsockname: %s\n", strerror(SOC_GetErrno())); + ftp_send_response(session, 451, "Failed to get connection info\r\n"); + ftp_session_destroy(session); + return; + } + + /* replace pasv port with data port */ + /* TODO: use global port variable so separate sessions don't collide */ + session->pasv_addr.sin_port = htons(DATA_PORT); + session->cmd_fd = new_fd; + + /* send initiator response */ + rc = ftp_send_response(session, 200, "Hello!\r\n"); + if(rc <= 0) + ftp_session_destroy(session); +} + +/*! accept PASV connection for ftp session + * + * @param[in] session ftp session + * + * @returns -1 for failure + */ +static int +ftp_session_accept(ftp_session_t *session) +{ + int new_fd; + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + + if(session->flags & SESSION_PASV) + { + /* clear PASV flag */ + session->flags &= ~SESSION_PASV; + + /* tell the peer that we're ready to accept the connection */ + ftp_send_response(session, 150, "Ready\r\n"); + + /* accept connection from peer */ + new_fd = accept(session->pasv_fd, (struct sockaddr*)&addr, &addrlen); + if(new_fd < 0) + { + console_print("accept: %s\n", strerror(SOC_GetErrno())); + ftp_session_close_pasv(session); + ftp_send_response(session, 425, "Failed to establish connection\r\n"); + return -1; + } + + console_print("accepted connection from %s:%u\n", + inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); + + ftp_session_set_state(session, DATA_TRANSFER_STATE); + session->data_fd = new_fd; + + return 0; + } + else + { + /* peer didn't send PASV command */ + ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + return -1; + } +} + +/*! connect to peer for ftp session + * + * @param[in] session ftp session + * + * @returns -1 for failure + */ +static int +ftp_session_connect(ftp_session_t *session) +{ + int rc; + + if(session->flags & SESSION_PORT) + { + /* clear PORT flag */ + session->flags &= ~SESSION_PORT; + + /* create a new socket */ + session->data_fd = socket(AF_INET, SOCK_STREAM, 0); + if(session->data_fd < 0) + { + console_print("socket: %s\n", strerror(SOC_GetErrno())); + ftp_send_response(session, 425, "Failed to establish connection\r\n"); + return -1; + } + + /* connect to peer */ + rc = connect(session->data_fd, (struct sockaddr*)&session->peer_addr, + sizeof(session->peer_addr)); + if(rc != 0) + { + console_print("connect: %s\n", strerror(SOC_GetErrno())); + ftp_closesocket(session->data_fd, 0); + session->data_fd = -1; + ftp_send_response(session, 425, "Failed to establish connection\r\n"); + return -1; + } + + console_print("connected to %s:%u\n", + inet_ntoa(session->peer_addr.sin_addr), + ntohs(session->peer_addr.sin_port)); + + /* tell peer that connection has been established */ + ftp_send_response(session, 150, "Ready\r\n"); + return 0; + } + else + { + /* peer didn't send PORT command */ + ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + return -1; + } +} + +/*! read command for ftp session + * + * @param[in] session ftp session + */ +static void +ftp_session_read_command(ftp_session_t *session) +{ + static char buffer[1024]; + ssize_t rc; + char *args; + ftp_command_t key, *command; + + memset(buffer, 0, sizeof(buffer)); + + /* retrieve command */ + rc = recv(session->cmd_fd, buffer, sizeof(buffer), 0); + if(rc < 0) + { + /* error retrieving command */ + console_print("recv: %s\n", strerror(SOC_GetErrno())); + ftp_session_close_cmd(session); + return; + } + if(rc == 0) + { + /* peer closed connection */ + ftp_session_close_cmd(session); + return; + } + else + { + /* split into command and arguments */ + /* TODO: support partial transfers */ + buffer[sizeof(buffer)-1] = 0; + + args = buffer; + while(*args && *args != '\r' && *args != '\n') + ++args; + *args = 0; + + args = buffer; + while(*args && !isspace((int)*args)) + ++args; + if(*args) + *args++ = 0; + + /* look up the command */ + key.name = buffer; + command = bsearch(&key, ftp_commands, + num_ftp_commands, sizeof(ftp_command_t), + ftp_command_cmp); + + /* execute the command */ + if(command == NULL) + ftp_send_response(session, 502, "invalid command\r\n"); + else + command->handler(session, args); + } +} + +/*! poll sockets for ftp session + * + * @param[in] session ftp session + * + * @returns next session + */ +static ftp_session_t* +ftp_session_poll(ftp_session_t *session) +{ + int rc; + struct pollfd pollinfo; + + switch(session->state) + { + case COMMAND_STATE: + /* we are waiting to read a command */ + pollinfo.fd = session->cmd_fd; + pollinfo.events = POLLIN; + pollinfo.revents = 0; + break; + + case DATA_CONNECT_STATE: + /* we are waiting for a PASV connection */ + pollinfo.fd = session->pasv_fd; + pollinfo.events = POLLIN; + pollinfo.revents = 0; + break; + + case DATA_TRANSFER_STATE: + /* we need to transfer data */ + pollinfo.fd = session->data_fd; + if(session->flags & SESSION_RECV) + pollinfo.events = POLLIN; + else + pollinfo.events = POLLOUT; + pollinfo.revents = 0; + break; + } + + /* poll the selected socket */ + rc = poll(&pollinfo, 1, 0); + if(rc < 0) + console_print("poll: %s\n", strerror(SOC_GetErrno())); + else if(rc > 0) + { + if(pollinfo.revents != 0) + { + /* handle event */ + switch(session->state) + { + case COMMAND_STATE: + if(pollinfo.revents & POLL_UNKNOWN) + console_print("cmd_fd: revents=0x%08X\n", pollinfo.revents); + + /* we need to read a new command */ + if(pollinfo.revents & (POLLERR|POLLHUP)) + ftp_session_close_cmd(session); + else if(pollinfo.revents & POLLIN) + ftp_session_read_command(session); + break; + + case DATA_CONNECT_STATE: + if(pollinfo.revents & POLL_UNKNOWN) + console_print("pasv_fd: revents=0x%08X\n", pollinfo.revents); + + /* we need to accept the PASV connection */ + if(pollinfo.revents & (POLLERR|POLLHUP)) + { + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 426, "Data connection failed\r\n"); + } + else if(pollinfo.revents & POLLIN) + { + if(ftp_session_accept(session) != 0) + ftp_session_set_state(session, COMMAND_STATE); + } + break; + + case DATA_TRANSFER_STATE: + if(pollinfo.revents & POLL_UNKNOWN) + console_print("data_fd: revents=0x%08X\n", pollinfo.revents); + + /* we need to transfer data */ + if(pollinfo.revents & (POLLERR|POLLHUP)) + { + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 426, "Data connection failed\r\n"); + } + else if(pollinfo.revents & (POLLIN|POLLOUT)) + session->transfer(session); + break; + } + } + } + + /* still connected to peer; return next session */ + if(session->cmd_fd >= 0) + return session->next; + + /* disconnected from peer; destroy it and return next session */ + return ftp_session_destroy(session); +} + +/*! initialize ftp subsystem */ +int +ftp_init(void) +{ + int rc; + +#ifdef _3DS + Result ret; + + /* initialize FS service */ + ret = fsInit(); + if(ret != 0) + { + console_print("fsInit: 0x%08X\n", (unsigned int)ret); + return -1; + } + + /* open SDMC archive */ + ret = FSUSER_OpenArchive(NULL, &sdmcArchive); + if(ret != 0) + { + console_print("FSUSER_OpenArchive: 0x%08X\n", (unsigned int)ret); + ret = fsExit(); + if(ret != 0) + console_print("fsExit: 0x%08X\n", (unsigned int)ret); + return -1; + } + + /* allocate buffer for SOC service */ + SOC_buffer = (u32*)memalign(SOC_ALIGN, SOC_BUFFERSIZE); + if(SOC_buffer == NULL) + { + console_print("memalign: failed to allocate\n"); + ret = FSUSER_CloseArchive(NULL, &sdmcArchive); + if(ret != 0) + console_print("FSUSER_CloseArchive: 0x%08X\n", (unsigned int)ret); + ret = fsExit(); + if(ret != 0) + console_print("fsExit: 0x%08X\n", (unsigned int)ret); + return -1; + } + + /* initialize SOC service */ + ret = SOC_Initialize(SOC_buffer, SOC_BUFFERSIZE); + if(ret != 0) + { + console_print("SOC_Initialize: 0x%08X\n", (unsigned int)ret); + free(SOC_buffer); + ret = FSUSER_CloseArchive(NULL, &sdmcArchive); + if(ret != 0) + console_print("FSUSER_CloseArchive: 0x%08X\n", (unsigned int)ret); + ret = fsExit(); + if(ret != 0) + console_print("fsExit: 0x%08X\n", (unsigned int)ret); + return -1; + } +#endif + + /* allocate socket to listen for clients */ + listenfd = socket(AF_INET, SOCK_STREAM, 0); + if(listenfd < 0) + { + console_print("socket: %s\n", strerror(SOC_GetErrno())); + ftp_exit(); + return -1; + } + + /* get address to listen on */ + serv_addr.sin_family = AF_INET; +#ifdef _3DS + serv_addr.sin_addr.s_addr = gethostid(); + serv_addr.sin_port = htons(LISTEN_PORT); +#else + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = htons(LISTEN_PORT); + + /* reuse address */ + { + int yes = 1; + rc = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + if(rc != 0) + { + console_print("setsockopt: %s\n", strerror(SOC_GetErrno())); + ftp_exit(); + return -1; + } + } +#endif + + /* bind socket to listen address */ + rc = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); + if(rc != 0) + { + console_print("bind: %s\n", strerror(SOC_GetErrno())); + ftp_exit(); + return -1; + } + + /* listen on socket */ + rc = listen(listenfd, 5); + if(rc != 0) + { + console_print("listen: %s\n", strerror(SOC_GetErrno())); + ftp_exit(); + return -1; + } + + /* print server address */ +#ifdef _3DS + console_set_status(STATUS_STRING " IP:%s Port:%u", + inet_ntoa(serv_addr.sin_addr), + ntohs(serv_addr.sin_port)); +#else + { + char hostname[128]; + socklen_t addrlen = sizeof(serv_addr); + rc = getsockname(listenfd, (struct sockaddr*)&serv_addr, &addrlen); + if(rc != 0) + { + console_print("getsockname: %s\n", strerror(SOC_GetErrno())); + ftp_exit(); + return -1; + } + + rc = gethostname(hostname, sizeof(hostname)); + if(rc != 0) + { + console_print("gethostname: %s\n", strerror(SOC_GetErrno())); + ftp_exit(); + return -1; + } + + console_set_status(STATUS_STRING " IP:%s Port:%u", + hostname, + ntohs(serv_addr.sin_port)); + } +#endif + + return 0; +} + +/*! deinitialize ftp subsystem */ +void +ftp_exit(void) +{ +#ifdef _3DS + Result ret; +#endif + + /* clean up all sessions */ + while(sessions != NULL) + ftp_session_destroy(sessions); + + /* stop listening for new clients */ + if(listenfd >= 0) + ftp_closesocket(listenfd, 0); + +#ifdef _3DS + /* deinitialize SOC service */ + ret = SOC_Shutdown(); + if(ret != 0) + console_print("SOC_Shutdown: 0x%08X\n", (unsigned int)ret); + free(SOC_buffer); + + /* deinitialize FS service */ + ret = fsExit(); + if(ret != 0) + console_print("fsExit: 0x%08X\n", (unsigned int)ret); +#endif +} + +int +ftp_loop(void) +{ + int rc; + struct pollfd pollinfo; + ftp_session_t *session; + + pollinfo.fd = listenfd; + pollinfo.events = POLLIN; + pollinfo.revents = 0; + + rc = poll(&pollinfo, 1, 0); + if(rc < 0) + console_print("poll: %s\n", strerror(SOC_GetErrno())); + else if(rc > 0) + { + if(pollinfo.revents & POLLIN) + { + ftp_session_new(listenfd); + } + else + { + console_print("listenfd: revents=0x%08X\n", pollinfo.revents); + } + } + + session = sessions; + while(session != NULL) + session = ftp_session_poll(session); + +#ifdef _3DS + hidScanInput(); + if(hidKeysDown() & KEY_B) + return -1; +#endif + + return 0; +} + +static void +cd_up(ftp_session_t *session) +{ + char *slash = NULL, *p; + + for(p = session->cwd; *p; ++p) + { + if(*p == '/') + slash = p; + } + *slash = 0; + if(strlen(session->cwd) == 0) + strcat(session->cwd, "/"); +} + +static int +validate_path(const char *args) +{ + const char *p; + + /* make sure no path components are '..' */ + p = args; + while((p = strstr(p, "/..")) != NULL) + { + if(p[3] == 0 || p[3] == '/') + return -1; + } + + /* make sure there are no '//' */ + if(strstr(args, "//") != NULL) + return -1; + + return 0; +} + +static void +build_path(ftp_session_t *session, + const char *args) +{ + char *p; + + memset(session->buffer, 0, sizeof(session->buffer)); + + if(args[0] == '/') + { + strncpy(session->buffer, args, sizeof(session->buffer)); + } + else + { + p = session->cwd + strlen(session->cwd); + while(*--p == '/') + *p = 0; + snprintf(session->buffer, sizeof(session->buffer), "%s/%s", + session->cwd, args); + p = session->buffer + strlen(session->buffer); + while(*--p == '/') + *p = 0; + } +} + +static void +list_transfer(ftp_session_t *session) +{ +#ifdef _3DS + Result ret; +#endif + ssize_t rc; + + if(session->bufferpos == session->buffersize) + { +#ifdef _3DS + FS_dirent dent; + u32 entries; + char name[256]; + + ret = FSDIR_Read(session->fd, &entries, 1, &dent); + if(ret != 0) + { + console_print("FSDIR_Read: 0x%08X\n", (unsigned int)ret); + ftp_session_close_cwd(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 450, "failed to read directory\r\n"); + return; + } + + if(entries == 0) + { + ftp_session_close_cwd(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 226, "OK\r\n"); + return; + } + + convert_name(name, dent.name); + if(strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + return; + + session->buffersize = + sprintf(session->buffer, + "%crwxrwxrwx 1 3DS 3DS %llu Jan 1 1970 %s\r\n", + dent.isDirectory ? 'd' : '-', + dent.fileSize, + name); +#else + struct stat st; + struct dirent *dent = readdir(session->dp); + if(dent == NULL) + { + ftp_session_close_cwd(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 226, "OK\r\n"); + return; + } + + if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) + return; + + snprintf(session->buffer, sizeof(session->buffer), + "%s/%s", session->cwd, dent->d_name); + rc = lstat(session->buffer, &st); + if(rc != 0) + { + console_print("stat '%s': %s\n", session->buffer, strerror(errno)); + ftp_session_close_cwd(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 550, "unavailable\r\n"); + return; + } + + session->buffersize = + sprintf(session->buffer, + "%crwxrwxrwx 1 3DS 3DS %llu Jan 1 1970 %s\r\n", + S_ISDIR(st.st_mode) ? 'd' : + S_ISLNK(st.st_mode) ? 'l' : '-', + (unsigned long long)st.st_size, + dent->d_name); +#endif + session->bufferpos = 0; + } + + rc = send(session->data_fd, session->buffer + session->bufferpos, + session->buffersize - session->bufferpos, 0); + if(rc <= 0) + { + if(rc < 0) + console_print("send: %s\n", strerror(SOC_GetErrno())); + else + console_print("send: %s\n", strerror(ECONNRESET)); + + ftp_session_close_cwd(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 426, "Connection broken during transfer\r\n"); + return; + } + + session->bufferpos += rc; +} + +static void +retrieve_transfer(ftp_session_t *session) +{ + ssize_t rc; + + if(session->bufferpos == session->buffersize) + { + rc = ftp_session_read_file(session); + if(rc <= 0) + { + ftp_session_close_file(session); + ftp_session_set_state(session, COMMAND_STATE); + if(rc < 0) + ftp_send_response(session, 451, "Failed to read file\r\n"); + else + ftp_send_response(session, 226, "OK\r\n"); + return; + } + + session->bufferpos = 0; + session->buffersize = rc; + } + + rc = send(session->data_fd, session->buffer + session->bufferpos, + session->buffersize - session->bufferpos, 0); + if(rc <= 0) + { + if(rc < 0) + console_print("send: %s\n", strerror(SOC_GetErrno())); + else + console_print("send: %s\n", strerror(ECONNRESET)); + + ftp_session_close_file(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 426, "Connection broken during transfer\r\n"); + return; + } + + session->bufferpos += rc; +} + +static void +store_transfer(ftp_session_t *session) +{ + ssize_t rc; + + if(session->bufferpos == session->buffersize) + { + rc = recv(session->data_fd, session->buffer, sizeof(session->buffer), 0); + if(rc <= 0) + { + if(rc < 0) + console_print("recv: %s\n", strerror(SOC_GetErrno())); + + ftp_session_close_file(session); + ftp_session_set_state(session, COMMAND_STATE); + + if(rc == 0) + ftp_send_response(session, 226, "OK\r\n"); + else + ftp_send_response(session, 426, "Connection broken during transfer\r\n"); + return; + } + + session->bufferpos = 0; + session->buffersize = rc; + } + + rc = ftp_session_write_file(session); + if(rc <= 0) + { + ftp_session_close_file(session); + ftp_session_set_state(session, COMMAND_STATE); + ftp_send_response(session, 451, "Failed to write file\r\n"); + return; + } + + session->bufferpos += rc; +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * F T P C O M M A N D S * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +FTP_DECLARE(ALLO) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 202, "superfluous command\r\n"); +} + +FTP_DECLARE(APPE) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(!(session->flags & SESSION_BINARY)) + return ftp_send_response(session, 450, "binary mode required\r\n"); + + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(CDUP) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + cd_up(session); + + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(CWD) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(strcmp(args, "..") == 0) + { + cd_up(session); + return ftp_send_response(session, 200, "OK\r\n"); + } + + if(validate_path(args) != 0) + return ftp_send_response(session, 553, "invalid file name\r\n"); + + build_path(session, args); + + { +#ifdef _3DS + Result ret; + Handle fd; + + ret = FSUSER_OpenDirectory(NULL, &fd, sdmcArchive, + FS_makePath(PATH_CHAR, session->buffer)); + if(ret != 0) + return ftp_send_response(session, 553, "not a directory\r\n"); + + ret = FSDIR_Close(fd); + if(ret != 0) + console_print("FSDIR_Close: 0x%08X\n", (unsigned int)ret); +#else + struct stat st; + int rc; + + rc = stat(session->buffer, &st); + if(rc != 0) + { + console_print("stat '%s': %s\n", session->buffer, strerror(errno)); + return ftp_send_response(session, 550, "unavailable\r\n"); + } + + if(!S_ISDIR(st.st_mode)) + return ftp_send_response(session, 553, "not a directory\r\n"); +#endif + } + + strncpy(session->cwd, session->buffer, sizeof(session->cwd)); + + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(DELE) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(FEAT) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 211, "\r\n"); +} + +FTP_DECLARE(LIST) +{ + ssize_t rc; + + console_print("%s %s\n", __func__, args ? args : ""); + + if(ftp_session_open_cwd(session) != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 550, "unavailable\r\n"); + } + + if(session->flags & SESSION_PORT) + { + ftp_session_set_state(session, DATA_TRANSFER_STATE); + rc = ftp_session_connect(session); + if(rc != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 425, "can't open data connection\r\n"); + } + } + else if(session->flags & SESSION_PASV) + ftp_session_set_state(session, DATA_CONNECT_STATE); + else + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + } + + session->flags &= ~(SESSION_RECV|SESSION_SEND); + session->flags |= SESSION_SEND; + + session->transfer = list_transfer; + session->bufferpos = 0; + session->buffersize = 0; + return 0; +} + +FTP_DECLARE(MKD) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(MODE) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(strcasecmp(args, "S") == 0) + return ftp_send_response(session, 200, "OK\r\n"); + + return ftp_send_response(session, 504, "unavailable\r\n"); +} + +FTP_DECLARE(NLST) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 504, "unavailable\r\n"); +} + +FTP_DECLARE(NOOP) +{ + console_print("%s %s\n", __func__, args ? args : ""); + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(PASS) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(strcasecmp(args, "anonymous") != 0) + return ftp_send_response(session, 430, "Invalid user name\r\n"); + + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(PASV) +{ + int rc; + char buffer[INET_ADDRSTRLEN + 10]; + char *p; + in_port_t port; + + console_print("%s %s\n", __func__, args ? args : ""); + + memset(buffer, 0, sizeof(buffer)); + + ftp_session_set_state(session, COMMAND_STATE); + + session->flags &= ~(SESSION_PASV|SESSION_PORT); + + session->pasv_fd = socket(AF_INET, SOCK_STREAM, 0); + if(session->pasv_fd < 0) + { + console_print("socket: %s\n", strerror(SOC_GetErrno())); + return ftp_send_response(session, 451, "\r\n"); + } + +#ifdef _3DS + console_print("binding to %s:%u\n", + inet_ntoa(session->pasv_addr.sin_addr), + ntohs(session->pasv_addr.sin_port)); +#endif + rc = bind(session->pasv_fd, (struct sockaddr*)&session->pasv_addr, + sizeof(session->pasv_addr)); + if(rc != 0) + { + console_print("bind: ret=%d errno=%d %s\n", + rc, SOC_GetErrno(), strerror(SOC_GetErrno())); + ftp_session_close_pasv(session); + return ftp_send_response(session, 451, "\r\n"); + } + + rc = listen(session->pasv_fd, 5); + if(rc != 0) + { + console_print("listen: %s\n", strerror(SOC_GetErrno())); + ftp_session_close_pasv(session); + return ftp_send_response(session, 451, "\r\n"); + } + +#ifndef _3DS + { + socklen_t addrlen = sizeof(session->pasv_addr); + rc = getsockname(session->pasv_fd, (struct sockaddr*)&session->pasv_addr, + &addrlen); + if(rc != 0) + { + console_print("getsockname: %s\n", strerror(SOC_GetErrno())); + ftp_session_close_pasv(session); + return ftp_send_response(session, 451, "\r\n"); + } + } +#endif + + console_print("listening on %s:%u\n", + inet_ntoa(session->pasv_addr.sin_addr), + ntohs(session->pasv_addr.sin_port)); + + session->flags |= SESSION_PASV; + + port = ntohs(session->pasv_addr.sin_port); + strcpy(buffer, inet_ntoa(session->pasv_addr.sin_addr)); + sprintf(buffer+strlen(buffer), ",%u,%u", + port >> 8, port & 0xFF); + for(p = buffer; *p; ++p) + { + if(*p == '.') + *p = ','; + } + + return ftp_send_response(session, 227, "%s\r\n", buffer); +} + +FTP_DECLARE(PORT) +{ + char *addrstr, *p, *portstr; + int commas = 0, rc; + short port = 0; + unsigned long val; + struct sockaddr_in addr; + + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + session->flags &= ~(SESSION_PASV|SESSION_PORT); + + addrstr = strdup(args); + if(addrstr == NULL) + return ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM)); + + for(p = addrstr; *p; ++p) + { + if(*p == ',') + { + if(commas != 3) + *p = '.'; + else + { + *p = 0; + portstr = p+1; + } + ++commas; + } + } + + if(commas != 5) + { + free(addrstr); + return ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); + } + + rc = inet_aton(addrstr, &addr.sin_addr); + if(rc == 0) + { + free(addrstr); + return ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); + } + + val = 0; + port = 0; + for(p = portstr; *p; ++p) + { + if(!isdigit((int)*p)) + { + if(p == portstr || *p != '.' || val > 0xFF) + { + free(addrstr); + return ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); + } + port <<= 8; + port += val; + val = 0; + } + else + { + val *= 10; + val += *p - '0'; + } + } + + if(val > 0xFF || port > 0xFF) + { + free(addrstr); + return ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); + } + port <<= 8; + port += val; + + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + + free(addrstr); + + memcpy(&session->peer_addr, &addr, sizeof(addr)); + + session->flags |= SESSION_PORT; + + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(PWD) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 200, "\"%s\"\r\n", session->cwd); +} + +FTP_DECLARE(QUIT) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_send_response(session, 221, "disconnecting\r\n"); + ftp_session_close_cmd(session); + + return 0; +} + +FTP_DECLARE(REST) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(!(session->flags & SESSION_BINARY)) + return ftp_send_response(session, 450, "binary mode required\r\n"); + + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(RETR) +{ + int rc; + + console_print("%s %s\n", __func__, args ? args : ""); + + if(!(session->flags & SESSION_BINARY)) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 450, "binary mode required\r\n"); + } + + if(validate_path(args) != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 553, "invalid file name\r\n"); + } + + build_path(session, args); + + if(ftp_session_open_file_read(session) != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 450, "failed to open file\r\n"); + } + + if(session->flags & SESSION_PORT) + { + ftp_session_set_state(session, DATA_TRANSFER_STATE); + rc = ftp_session_connect(session); + if(rc != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 425, "can't open data connection\r\n"); + } + } + else if(session->flags & SESSION_PASV) + ftp_session_set_state(session, DATA_CONNECT_STATE); + else + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + } + + session->flags &= ~(SESSION_RECV|SESSION_SEND); + session->flags |= SESSION_SEND; + + session->transfer = retrieve_transfer; + session->bufferpos = 0; + session->buffersize = 0; + return 0; +} + +FTP_DECLARE(RMD) +{ + /* TODO */ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(RNFR) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + session->flags &= ~SESSION_RENAME; + + if(validate_path(args) != 0) + return ftp_send_response(session, 553, "invalid file name\r\n"); + + build_path(session, args); + + session->flags |= SESSION_RENAME; + + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(RNTO) +{ + char buffer[1024]; + + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(!(session->flags & SESSION_RENAME)) + return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + + session->flags &= ~SESSION_RENAME; + + memcpy(buffer, session->buffer, 1024); + + if(validate_path(args) != 0) + return ftp_send_response(session, 554, "invalid file name\r\n"); + + build_path(session, args); + + /* TODO perform rename(buffer, session->buffer) */ + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(STOR) +{ + int rc; + + console_print("%s %s\n", __func__, args ? args : ""); + + if(!(session->flags & SESSION_BINARY)) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 450, "binary mode required\r\n"); + } + + if(validate_path(args) != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 553, "invalid file name\r\n"); + } + + build_path(session, args); + + if(ftp_session_open_file_write(session) != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 450, "failed to open file\r\n"); + } + + if(session->flags & SESSION_PORT) + { + ftp_session_set_state(session, DATA_TRANSFER_STATE); + rc = ftp_session_connect(session); + if(rc != 0) + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 425, "can't open data connection\r\n"); + } + } + else if(session->flags & SESSION_PASV) + ftp_session_set_state(session, DATA_CONNECT_STATE); + else + { + ftp_session_set_state(session, COMMAND_STATE); + return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + } + + session->flags &= ~(SESSION_RECV|SESSION_SEND); + session->flags |= SESSION_RECV; + + session->transfer = store_transfer; + session->bufferpos = 0; + session->buffersize = 0; + return 0; +} + +FTP_DECLARE(STOU) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(!(session->flags & SESSION_BINARY)) + return ftp_send_response(session, 450, "binary mode required\r\n"); + + return ftp_send_response(session, 502, "unavailable\r\n"); +} + +FTP_DECLARE(STRU) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(strcasecmp(args, "F") == 0) + return ftp_send_response(session, 200, "OK\r\n"); + + return ftp_send_response(session, 504, "unavailable\r\n"); +} + +FTP_DECLARE(SYST) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + return ftp_send_response(session, 215, "UNIX Type: L8\r\n"); +} + +FTP_DECLARE(TYPE) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(strcasecmp("I", args) != 0 + && strcasecmp("I 8", args) != 0) + return ftp_send_response(session, 504, "unavailable\r\n"); + + session->flags |= SESSION_BINARY; + + return ftp_send_response(session, 200, "OK\r\n"); +} + +FTP_DECLARE(USER) +{ + console_print("%s %s\n", __func__, args ? args : ""); + + ftp_session_set_state(session, COMMAND_STATE); + + if(strcasecmp(args, "anonymous") != 0) + return ftp_send_response(session, 430, "Invalid user name\r\n"); + + return ftp_send_response(session, 200, "OK\r\n"); +} diff --git a/source/main.c b/source/main.c new file mode 100644 index 0000000..a20fdd7 --- /dev/null +++ b/source/main.c @@ -0,0 +1,114 @@ +#include +#include +#include +#ifdef _3DS +#include <3ds.h> +#endif +#include "console.h" +#include "ftp.h" + +/*! looping mechanism + * + * @param[in] callback function to call during each iteration + */ +static void +loop(int (*callback)(void)) +{ +#ifdef _3DS + int rc; + APP_STATUS status; + + /* check apt status */ + while((status = aptGetStatus()) != APP_EXITING) + { + rc = 0; + if(status == APP_RUNNING) + rc = callback(); + else if(status == APP_SUSPENDING) + aptReturnToMenu(); + else if(status == APP_SLEEPMODE) + aptWaitStatusEvent(); + + if(rc == 0) + console_render(); + else + return; + } +#else + for(;;) + callback(); +#endif +} + +/*! wait until the B button is pressed + * + * @returns -1 if B was pressed + */ +static int +wait_for_b(void) +{ +#ifdef _3DS + /* update button state */ + hidScanInput(); + + /* check if B was pressed */ + if(hidKeysDown() & KEY_B) + return -1; + + /* B was not pressed */ + return 0; +#else + return -1; +#endif +} + +/*! entry point + * + * @param[in] argc unused + * @param[in] argv unused + * + * returns exit status + */ +int +main(int argc, + char *argv[]) +{ +#ifdef _3DS + /* initialize needed 3DS services */ + srvInit(); + aptInit(); + hidInit(NULL); + irrstInit(NULL); + gfxInit(); + gfxSet3D(false); +#endif + + /* initialize console subsystem */ + console_init(); + console_set_status(STATUS_STRING); + + /* initialize ftp subsystem */ + if(ftp_init() == 0) + { + /* ftp loop */ + loop(ftp_loop); + + /* done with ftp */ + ftp_exit(); + } + + console_print("Press B to exit\n"); + loop(wait_for_b); + console_exit(); + +#ifdef _3DS + /* deinitialize 3DS services */ + gfxExit(); + irrstExit(); + hidExit(); + aptExit(); + srvExit(); +#endif + + return 0; +}