/*
 *
 * gfxboot.c
 *
 * A com32 module to load gfxboot graphics.
 *
 * Copyright (c) 2009 Steffen Winterfeldt.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, Inc., 53 Temple Place Ste 330, Boston MA
 * 02111-1307, USA; either version 2 of the License, or (at your option) any
 * later version; incorporated herein by reference.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <syslinux/loadfile.h>
#include <syslinux/config.h>
#include <syslinux/linux.h>
#include <syslinux/boot.h>
#include <console.h>
#include <com32.h>


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define MAX_CONFIG_LINE_LEN	2048
#define MAX_CMDLINE_LEN		2048

// buffer for realmode callback
// must be at least block size; can in theory be larger than 4k, but there's
// not enough space left
#define REALMODE_BUF_SIZE	4096

// gfxboot working memory in MB
#define	GFX_MEMORY_SIZE		7

// read chunk size for progress bar
#define CHUNK_SIZE	(64 << 10)

// callback function numbers
#define GFX_CB_INIT		0
#define GFX_CB_DONE		1
#define GFX_CB_INPUT		2
#define GFX_CB_MENU_INIT	3
#define GFX_CB_INFOBOX_INIT	4
#define GFX_CB_INFOBOX_DONE	5
#define GFX_CB_PROGRESS_INIT	6
#define GFX_CB_PROGRESS_DONE	7
#define GFX_CB_PROGRESS_UPDATE	8
#define GFX_CB_PROGRESS_LIMIT	9		// unused
#define GFX_CB_PASSWORD_INIT	10
#define GFX_CB_PASSWORD_DONE	11

// real mode code chunk, will be placed into bounce buffer
extern void realmode_callback_start, realmode_callback_end;

// gets in the way
#undef linux


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// gfxboot config data (64 bytes)
typedef struct __attribute__ ((packed)) {
  uint8_t bootloader;		//  0: boot loader type (0: lilo, 1: syslinux, 2: grub)
  uint8_t sector_shift;		//  1: sector shift
  uint8_t media_type;		//  2: media type (0: disk, 1: floppy, 2: cdrom)
  uint8_t failsafe;		//  3: turn on failsafe mode (bitmask)
				//    0: SHIFT pressed
				//    1: skip gfxboot
				//    2: skip monitor detection
  uint8_t sysconfig_size;	//  4: size of sysconfig data
  uint8_t boot_drive;		//  5: BIOS boot drive
  uint16_t callback;		//  6: offset to callback handler
  uint16_t bootloader_seg;	//  8: code/data segment used by bootloader; must follow gfx_callback
  uint16_t serial_port;		// 10: syslinux initialized serial port from 'serial' option
  uint32_t user_info_0;		// 12: data for info box
  uint32_t user_info_1;		// 16: data for info box
  uint32_t bios_mem_size;	// 20: BIOS memory size (in bytes)
  uint16_t xmem_0;		// 24: extended mem area 0 (start:size in MB; 12:4 bits) - obsolete
  uint16_t xmem_1;		// 26: extended mem area 1 - obsolete
  uint16_t xmem_2;		// 28: extended mem area 2 - obsolete
  uint16_t xmem_3;		// 30: extended mem area 3 - obsolete
  uint32_t file;		// 32: start of gfx file
  uint32_t archive_start;	// 36: start of cpio archive
  uint32_t archive_end;		// 40: end of cpio archive
  uint32_t mem0_start;		// 44: low free memory start
  uint32_t mem0_end;		// 48: low free memory end
  uint32_t xmem_start;		// 52: extended mem start
  uint32_t xmem_end;		// 56: extended mem end
  uint16_t features;		// 60: feature flags returned by GFX_CB_INIT
  				//    0: GFX_CB_MENU_INIT accepts 32 bit addresses
  				//    1: knows about xmem_start, xmem_end
  uint16_t reserved_1;		// 62:
} gfx_config_t;


// gfxboot menu description (18 bytes)
typedef struct __attribute__ ((packed)) {
  uint16_t entries;
  char *default_entry;
  char *label_list;
  uint16_t label_size;
  char *arg_list;
  uint16_t arg_size;
} gfx_menu_t;


// menu description
typedef struct menu_s {
  struct menu_s *next;
  char *label;
  char *kernel;
  char *linux;
  char *localboot;
  char *initrd;
  char *append;
  char *ipappend;
} menu_t;


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
gfx_config_t gfx_config;
gfx_menu_t gfx_menu;

menu_t *menu;
menu_t *menu_default;

struct {
  uint32_t jmp_table[12];
  uint16_t code_seg;
  char fname_buf[64];
} gfx;

void *lowmem_buf;
unsigned lowmem_buf_size;

int timeout;

char cmdline[MAX_CMDLINE_LEN];

void *save_buf;
unsigned save_buf_size;


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void show_message(char *file);
char *get_config_file_name(void);
char *skip_spaces(char *s);
char *skip_nonspaces(char *s);
void chop_line(char *s);
int read_config_file(void);
unsigned magic_ok(unsigned char *buf, unsigned *code_size);
unsigned find_file(unsigned char *buf, unsigned len, unsigned *gfx_file_start, unsigned *file_len, unsigned *code_size);
int gfx_init(char *file);
int gfx_menu_init(void);
void gfx_done(void);
int gfx_input(void);
ssize_t save_read(int fd, void *buf, size_t size);
void *load_one(char *file, ssize_t *file_size);
void boot(void);
void boot_entry(menu_t *menu_ptr, char *arg);


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
int main(int argc, char **argv)
{
  int menu_index;
  enum syslinux_filesystem syslinux_id;
  com32sys_t r;

  openconsole(&dev_stdcon_r, &dev_stdcon_w);

  syslinux_id = syslinux_version()->filesystem;

  lowmem_buf = __com32.cs_bounce;
  lowmem_buf_size = __com32.cs_bounce_size;

  r.eax.l = 0x0a;	// Get Derivative-Specific Information
  r.ecx.l = 9;
  __intcall(0x22, &r, &r);
  gfx_config.sector_shift = (uint8_t) r.ecx.l;
  gfx_config.boot_drive = (uint8_t) r.edx.l;

  if(syslinux_id == SYSLINUX_FS_PXELINUX) {
    gfx_config.sector_shift = 11;
    gfx_config.boot_drive = 0;
  }

  gfx_config.bootloader = 1;
  gfx_config.sysconfig_size = sizeof gfx_config;
  gfx_config.bootloader_seg = 0;	// apparently not needed

  save_buf_size = lowmem_buf_size;
  save_buf = malloc(save_buf_size);

  if(argc < 2) {
    printf("Usage: gfxboot.c32 bootlogo_file [message_file]\n");
    if(argc > 2) show_message(argv[2]);

    return 0;
  }

  if(read_config_file()) {
    printf("Error reading config file\n");
    if(argc > 2) show_message(argv[2]);

    return 0;
  }

  if(gfx_init(argv[1])) {
    printf("Error setting up gfxboot\n");
    if(argc > 2) show_message(argv[2]);

    return 0;
  }

  gfx_menu_init();

  for(;;) {
    menu_index = gfx_input();

    // abort gfx, return to text mode prompt
    if(menu_index == -1) {
      gfx_done();
      break;
    }

    // does not return if it succeeds
    boot();
  }

  if(argc > 2) show_message(argv[2]);

  return 0;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void show_message(char *file)
{
  int c;
  FILE *f;

  if(!(f = fopen(file, "r"))) return;

  while((c = getc(f)) != EOF) {
    if(c < ' ' && c != '\n' && c != '\t') continue;
    printf("%c", c);
  }

  fclose(f);
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
char *skip_spaces(char *s)
{
  while(*s && (*s == ' ' || *s == '\t')) s++;

  return s;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
char *skip_nonspaces(char *s)
{
  while(*s && *s != ' ' && *s != '\t') s++;

  return s;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void chop_line(char *s)
{
  int i = strlen(s);

  if(!i) return;

  while(--i >= 0) {
    if(s[i] == ' ' || s[i] == '\t' || s[i] == '\n') {
      s[i] = 0;
    }
    else {
      break;
    }
  }
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Read and parse syslinux config file.
//
// return:
//   0: ok, 1: error
//
int read_config_file(void)
{
  FILE *f;
  char *s, *t, buf[MAX_CONFIG_LINE_LEN];
  unsigned u, menu_idx = 0, label_size = 0, append_size = 0;
  menu_t *menu_ptr = NULL, **menu_next = &menu;

  menu_default = calloc(1, sizeof *menu_default);

  if(!(f = fopen(syslinux_config_file(), "r"))) return 1;

  while((s = fgets(buf, sizeof buf, f))) {
    chop_line(s);
    s = skip_spaces(s);
    if(!*s || *s == '#') continue;
    t = skip_nonspaces(s);
    if(*t) *t++ = 0;
    t = skip_spaces(t);

    if(!strcmp(s, "timeout")) {
      timeout = atoi(t);
      continue;
    }

    if(!strcmp(s, "default")) {
      menu_default->label = strdup(t);
      u = strlen(t);
      if(u > label_size) label_size = u;
      continue;
    }

    if(!strcmp(s, "label")) {
      menu_ptr = *menu_next = calloc(1, sizeof **menu_next);
      menu_next = &menu_ptr->next;
      menu_idx++;
      menu_ptr->label = strdup(t);
      u = strlen(t);
      if(u > label_size) label_size = u;
      continue;
    }

    if(!strcmp(s, "kernel") && menu_ptr) {
      menu_ptr->kernel = strdup(t);
      continue;
    }

    if(!strcmp(s, "linux") && menu_ptr) {
      menu_ptr->linux = strdup(t);
      continue;
    }

    if(!strcmp(s, "localboot") && menu_ptr) {
      menu_ptr->localboot = strdup(t);
      continue;
    }

    if(!strcmp(s, "initrd") && menu_ptr) {
      menu_ptr->initrd = strdup(t);
      continue;
    }

    if(!strcmp(s, "append")) {
      (menu_ptr ?: menu_default)->append = strdup(t);
      u = strlen(t);
      if(u > append_size) append_size = u;
      continue;
    }

    if(!strcmp(s, "ipappend")) {
      (menu_ptr ?: menu_default)->ipappend = strdup(t);
      continue;
    }
  }

  fclose(f);

  // final '\0'
  label_size++;
  append_size++;

  gfx_menu.entries = menu_idx;
  gfx_menu.label_size = label_size;
  gfx_menu.arg_size = append_size;

  gfx_menu.default_entry = menu_default->label;
  if(!gfx_menu.default_entry && menu) {
    gfx_menu.default_entry = menu->label;
  }

  gfx_menu.label_list = calloc(menu_idx, label_size);
  gfx_menu.arg_list = calloc(menu_idx, append_size);

  for(u = 0, menu_ptr = menu; menu_ptr; menu_ptr = menu_ptr->next, u++) {
    if(!menu_ptr->append) menu_ptr->append = menu_default->append;
    if(!menu_ptr->ipappend) menu_ptr->ipappend = menu_default->ipappend;

    if(menu_ptr->label) strcpy(gfx_menu.label_list + u * label_size, menu_ptr->label);
    if(menu_ptr->append) strcpy(gfx_menu.arg_list + u * append_size, menu_ptr->append);
  }

  return 0;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Check header and return code start offset.
//
unsigned magic_ok(unsigned char *buf, unsigned *code_size)
{
  if(
    *(unsigned *) buf == 0x0b2d97f00 &&		// magic id
    (buf[4] == 8)				// version 8
  ) {
    *code_size = *(unsigned *) (buf + 12);
    return *(unsigned *) (buf + 8);
  }

  return 0;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Search (cpio archive) for gfx file.
//
unsigned find_file(unsigned char *buf, unsigned len, unsigned *gfx_file_start, unsigned *file_len, unsigned *code_size)
{
  unsigned i, fname_len, code_start = 0;

  *gfx_file_start = 0;
  *code_size = 0;

  if((code_start = magic_ok(buf, code_size))) return code_start;

  for(i = 0; i < len;) {
    if((len - i) >= 0x1a && (buf[i] + (buf[i + 1] << 8)) == 0x71c7) {
      fname_len = *(unsigned short *) (buf + i + 20);
      *file_len = *(unsigned short *) (buf + i + 24) + (*(unsigned short *) (buf + i + 22) << 16);
      i += 26 + fname_len;
      i = ((i + 1) & ~1);
      if((code_start = magic_ok(buf + i, code_size))) {
        *gfx_file_start = i;
        return code_start;
      }
      i += *file_len;
      i = ((i + 1) & ~1);
    }
    else {
      break;
    }
  }

  return code_start;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Initialize gfxboot code.
//
// return:
//   0: ok, 1: error
//
int gfx_init(char *file)
{
  size_t archive_size = 0;
  void *archive;
  unsigned code_start, code_size, file_start, file_len, u;
  com32sys_t r;
  void *lowmem = lowmem_buf;
  unsigned lowmem_size = lowmem_buf_size;

  printf("Loading %s...\n", file);
  if(loadfile(file, &archive, &archive_size)) return 1;

  if(!archive_size) return 1;

  // printf("%s: %d\n", file, archive_size);

  gfx_config.archive_start = (uint32_t) archive;
  gfx_config.archive_end = gfx_config.archive_start + archive_size;

  // locate file inside cpio archive
  if(!(code_start = find_file(archive, archive_size, &file_start, &file_len, &code_size))) {
    printf("%s: invalid file format\n", file);
    return 1;
  }

#if 0
  printf(
    "code_start = 0x%x, code_size = 0x%x\n"
    "archive_start = 0x%x, archive size = 0x%x\n"
    "file_start = 0x%x, file_len = 0x%x\n",
    code_start, code_size,
    gfx_config.archive_start, archive_size,
    file_start, file_len
  );
#endif

  gfx_config.file = gfx_config.archive_start + file_start;

  u = &realmode_callback_end - &realmode_callback_start;
  u = (u + REALMODE_BUF_SIZE + 0xf) & ~0xf;

  if(u + code_size > lowmem_size) {
    printf("bounce buffer too small: size %u, needed %u\n", lowmem_size, u + code_size);
    return 1;
  }

  memcpy(lowmem + REALMODE_BUF_SIZE, &realmode_callback_start, &realmode_callback_end - &realmode_callback_start);

  // fill in buffer size and location
  *(uint16_t *) (lowmem + REALMODE_BUF_SIZE) = REALMODE_BUF_SIZE;
  *(uint16_t *) (lowmem + REALMODE_BUF_SIZE + 2) = (uint32_t) lowmem >> 4;

  gfx_config.bootloader_seg = ((uint32_t) lowmem + REALMODE_BUF_SIZE) >> 4;
  gfx_config.callback = 4;	// start address

  lowmem += u;
  lowmem_size -= u;

  memcpy(lowmem, archive + file_start + code_start, code_size);

  gfx_config.mem0_start = (uint32_t) lowmem + code_size;
  gfx_config.mem0_end = (uint32_t) lowmem + lowmem_size;
  // align a bit
  gfx_config.mem0_start = (gfx_config.mem0_start + 0xf) & ~0xf;

  gfx_config.xmem_start = (uint32_t) malloc(GFX_MEMORY_SIZE << 20);
  if(gfx_config.xmem_start) {
    gfx_config.xmem_end = gfx_config.xmem_start + (GFX_MEMORY_SIZE << 20);
  }

  // fake; not used anyway
  gfx_config.bios_mem_size = 256 << 20;

  gfx.code_seg = (uint32_t) lowmem >> 4;

  for(u = 0; u < sizeof gfx.jmp_table / sizeof *gfx.jmp_table; u++) {
    gfx.jmp_table[u] = (gfx.code_seg << 16) + *(uint16_t *) (lowmem + 2 * u);
  }

#if 0
  for(u = 0; u < sizeof gfx.jmp_table / sizeof *gfx.jmp_table; u++) {
    printf("%d: 0x%08x\n", u, gfx.jmp_table[u]);
  }
#endif

  // we are ready to start

  r.esi.l = (uint32_t) &gfx_config;
  __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_INIT], &r, &r);

  if((r.eflags.l & EFLAGS_CF)) {
    printf("graphics initialization failed\n");

    return 1;
  }

  if((gfx_config.features & 3) != 3) {
    gfx_done();

    printf("%s: boot graphics code too old, please use newer version\n", file);

    return 1;
  }


  return 0;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
int gfx_menu_init(void)
{
  com32sys_t r;

  r.esi.l = (uint32_t) &gfx_menu;
  __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_MENU_INIT], &r, &r);

  return 0;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void gfx_done(void)
{
  com32sys_t r;

  __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_DONE], &r, &r);
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Run gfxboot main loop.
//
// return:
//   boot menu index (-1: go to text mode prompt)
//
int gfx_input(void)
{
  com32sys_t r;

  r.edi.l = (uint32_t) cmdline;
  r.ecx.l = sizeof cmdline;
  r.eax.l = timeout * 182 / 100;
  timeout = 0;		// use timeout only first time
  __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_INPUT], &r, &r);
  if((r.eflags.l & EFLAGS_CF)) r.eax.l = 1;

  if(r.eax.l == 1) return -1;

  return r.ebx.l;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Like read(2) but preserve bounce buffer.
//
ssize_t save_read(int fd, void *buf, size_t size)
{
  ssize_t i;

  memcpy(save_buf, lowmem_buf, save_buf_size);
  i = read(fd, buf, size);
  memcpy(lowmem_buf, save_buf, save_buf_size);

  return i;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Read file and update progress bar.
//
void *load_one(char *file, ssize_t *file_size)
{
  int fd;
  void *buf = NULL;
  struct stat sbuf;
  ssize_t size = 0, cur, i;
  com32sys_t r;

  *file_size = 0;

  if((fd = open(file, O_RDONLY)) == -1) {
    printf("%s: file not found\n", file);
    return buf;
  }

  if(!fstat(fd, &sbuf) && S_ISREG(sbuf.st_mode)) size = sbuf.st_size;

  i = 0;

  if(size) {
    buf = malloc(size);
    for(i = 1, cur = 0 ; cur < size && i > 0; cur += i) {
      i = save_read(fd, buf + cur, CHUNK_SIZE);
      r.eax.l = i >> gfx_config.sector_shift;
      __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_PROGRESS_UPDATE], &r, &r);
    }
  }
  else {
    do {
      buf = realloc(buf, size + CHUNK_SIZE);
      i = save_read(fd, buf + size, CHUNK_SIZE);
      size += i;
      r.eax.l = i >> gfx_config.sector_shift;
      __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_PROGRESS_UPDATE], &r, &r);
    } while(i > 0);
  }

  close(fd);

  if(i == -1) {
    free(buf);
    buf = NULL;
    size = 0;
  }

  *file_size = size;

  return buf;
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Locate menu entry and boot.
//
void boot(void)
{
  char *label, *arg, *s;
  menu_t *menu_ptr;

  label = skip_spaces(cmdline);
  arg = skip_spaces(s = skip_nonspaces(label));
  *s = 0;

  for(menu_ptr = menu; menu_ptr; menu_ptr = menu_ptr->next) {
    if(menu_ptr->label && !strcmp(menu_ptr->label, label)) break;
  }

  boot_entry(menu_ptr, arg);
}


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Load & run kernel.
//
// Returns only on error.
//
void boot_entry(menu_t *menu_ptr, char *arg)
{
  void *kernel, *initrd_buf;
  ssize_t kernel_size = 0, initrd_size = 0;
  struct initramfs *initrd = NULL;
  char *file, *cmd_buf;
  int fd;
  struct stat sbuf;
  com32sys_t r;
  char *s, *s0, *t, *initrd_arg;

  if(!menu_ptr) return;

  if(menu_ptr->localboot) {
    gfx_done();
    syslinux_local_boot(atoi(arg));

    return;
  }

  file = menu_ptr->kernel;
  if(!file) file = menu_ptr->linux;
  if(!file) {
    gfx_done();
    asprintf(&cmd_buf, "%s %s", menu_ptr->label, arg);
    syslinux_run_command(cmd_buf);
    return;
  }

  // first, load kernel

  r.eax.l = 0;		// kernel size in sectors

  if((fd = open(file, O_RDONLY)) >= 0) {
    if(!fstat(fd, &sbuf) && S_ISREG(sbuf.st_mode)) r.eax.l = sbuf.st_size >> gfx_config.sector_shift;
    close(fd);
  }

  r.esi.l = (uint32_t) file;
  __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_PROGRESS_INIT], &r, &r);

  kernel = load_one(file, &kernel_size);

  if(!kernel) {
    gfx_done();
    printf("%s: read error\n", file);
    return;
  }

  if(kernel_size < 1024 || *(uint32_t *) (kernel + 0x202) != 0x53726448) {
    // not a linux kernel
    gfx_done();
    asprintf(&cmd_buf, "%s %s", menu_ptr->label, arg);
    syslinux_run_command(cmd_buf);
    return;
  }

  // printf("kernel = %p, size = %d\n", kernel, kernel_size);

  // parse cmdline for "initrd" option

  initrd_arg = menu_ptr->initrd;

  s = s0 = strdup(arg);

  while(*s && strncmp(s, "initrd=", sizeof "initrd=" - 1)) {
    s = skip_spaces(skip_nonspaces(s));
  }

  if(*s) {
    s += sizeof "initrd=" - 1;
    *skip_nonspaces(s) = 0;
    initrd_arg = s;
  }

  if(initrd_arg) {
    initrd = initramfs_init();

    while((t = strsep(&s, ","))) {
      initrd_buf = load_one(t, &initrd_size);

      if(!initrd_buf) {
        printf("%s: read error\n", t);
        free(s0);
        return;
      }

      initramfs_add_data(initrd, initrd_buf, initrd_size, initrd_size, 4);

      // printf("initrd = %p, size = %d\n", initrd_buf, initrd_size);
    }
  }

  free(s0);

  __farcall(gfx.code_seg, gfx.jmp_table[GFX_CB_PROGRESS_DONE], &r, &r);

  syslinux_boot_linux(kernel, kernel_size, initrd, arg);
}


