commit 6d4b2593241ef9d0d5d49a3b57a43d4da0150a32 Author: mtheall Date: Sun Nov 23 16:39:00 2014 -0600 initial commit 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 0000000..f6b70eb Binary files /dev/null and b/data/sans.10.kerning.bin differ diff --git a/data/sans.10.render.bin b/data/sans.10.render.bin new file mode 100644 index 0000000..08da873 Binary files /dev/null and b/data/sans.10.render.bin differ diff --git a/data/sans.12.kerning.bin b/data/sans.12.kerning.bin new file mode 100644 index 0000000..843d796 Binary files /dev/null and b/data/sans.12.kerning.bin differ diff --git a/data/sans.12.render.bin b/data/sans.12.render.bin new file mode 100644 index 0000000..74912e9 Binary files /dev/null and b/data/sans.12.render.bin differ diff --git a/data/sans.14.kerning.bin b/data/sans.14.kerning.bin new file mode 100644 index 0000000..e89a9fe Binary files /dev/null and b/data/sans.14.kerning.bin differ diff --git a/data/sans.14.render.bin b/data/sans.14.render.bin new file mode 100644 index 0000000..c5b6d09 Binary files /dev/null and b/data/sans.14.render.bin differ diff --git a/data/sans.16.kerning.bin b/data/sans.16.kerning.bin new file mode 100644 index 0000000..be40c26 Binary files /dev/null and b/data/sans.16.kerning.bin differ diff --git a/data/sans.16.render.bin b/data/sans.16.render.bin new file mode 100644 index 0000000..a93c756 Binary files /dev/null and b/data/sans.16.render.bin differ diff --git a/data/sans.8.kerning.bin b/data/sans.8.kerning.bin new file mode 100644 index 0000000..f95aff5 Binary files /dev/null and b/data/sans.8.kerning.bin differ diff --git a/data/sans.8.render.bin b/data/sans.8.render.bin new file mode 100644 index 0000000..9d1dd7c Binary files /dev/null and b/data/sans.8.render.bin differ diff --git a/ftbrony.png b/ftbrony.png new file mode 100644 index 0000000..0081995 Binary files /dev/null and b/ftbrony.png differ 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; +}