Index: tools/sap/Makefile
===================================================================
--- tools/sap/Makefile	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/Makefile	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,53 @@
+##########################################################################
+# targets:                                                               #
+#  all (default) : build sap2 and sapfs                                  #
+#  install       : build and install sap2 and sapfs in $(HOME)/emu/sap   #
+#  clean         : delete all rebuildable files                          #
+########################################################################## 
+
+
+################
+# object files #
+################ 
+
+OBJ_DIR =
+OBJ_SAP = $(OBJ_DIR)sap2.o $(OBJ_DIR)libsap.o $(OBJ_DIR)lfloppy.o
+OBJ_SAPFS = $(OBJ_DIR)sapfs.o $(OBJ_DIR)libsap.o
+
+
+##################
+# compiler flags #
+##################
+
+CC = gcc
+
+WFLAGS = -Wall -W -Werror
+
+OFLAGS = -O2 
+
+CFLAGS = $(WFLAGS) $(OFLAGS)
+
+
+################
+# target rules #
+################
+
+.PHONY: all install clean
+
+all: sap2 sapfs fd2sap
+
+clean:
+	rm -f sap2 sapfs fd2sap $(OBJ_DIR)*.o
+
+sap2: $(OBJ_SAP) 
+	$(CC) -s -o sap2 $(OBJ_SAP)
+
+sapfs: $(OBJ_SAPFS)
+	$(CC) -s -o sapfs $(OBJ_SAPFS)
+
+fd2sap: 
+	$(CC) -s -o fd2sap fd2sap.c
+
+$(OBJ_DIR)%.o: %.c
+	$(CC) -c $(CFLAGS) -o $@ $<
+
Index: tools/sap/dfloppy.c
===================================================================
--- tools/sap/dfloppy.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/dfloppy.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,291 @@
+/*  Lecture des disquettes Thomson sous MS-DOS
+ *  Version 2.1
+ *  Copyright (C) 2000-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+/* Limitations:
+ *  - les faces 1 des disquettes ne sont pas accessibles,
+ *  - les disquettes simple densité (format FM) ne sont pas lisibles.
+ */
+
+#ifndef SCAN_DEPEND
+   #include <stdio.h>
+   #include <stddef.h>
+   #include <bios.h>
+   #include <dpmi.h>
+   #include <sys/movedata.h>
+#endif
+
+#include "floppy.h"
+
+
+#define DISK_RETRY     3
+
+struct floppy_cmd {
+   int cmd;
+   int head;
+   int track;
+   int sector;
+   int nsects;
+   void *buffer;
+};
+
+static int drive_type[2];
+static int dpt_addr = 0;
+
+#define DPT_SIZE      11
+
+static unsigned char pc_dpt[DPT_SIZE];
+
+
+
+/* SetDiskParameters:
+ *  Initialise les paramètres disquette pour le format Thomson.
+ */
+static void SetDiskParameters(int density)
+{
+   unsigned char to_dpt[DPT_SIZE];
+
+   if (!dpt_addr) {
+      /* on récupère le vecteur 0x1E du BIOS qui pointe sur la table
+         des paramètres de la disquette */
+      _dosmemgetl(0x1E*4, 1, &dpt_addr);
+
+      /* on sauvegarde les paramètres originaux */
+      _dosmemgetb(dpt_addr, DPT_SIZE, pc_dpt);
+   }
+
+   /* on fixe les nouveaux paramètres */
+   to_dpt[0x0]=0xDF;  /* spec1                      */
+   to_dpt[0x1]=0x02;  /* spec2                      */
+   to_dpt[0x2]=0x25;  /* motor turn off delay       */
+   to_dpt[0x3]=density - 1;  /* sector size = 128*2^n */
+   to_dpt[0x4]=0x10;  /* 16 sectors/track           */
+   to_dpt[0x5]=0x1B;  /* gap between sectors        */
+   to_dpt[0x6]=(density == 1 ? 0x80 : 0xFF);  /* data length */
+   to_dpt[0x7]=0x2C;  /* gap when formatting        */
+   to_dpt[0x8]=0xE5;  /* filler byte                */
+   to_dpt[0x9]=0x0F;  /* head settle time           */
+   to_dpt[0xA]=0x08;  /* motor start time           */
+   _dosmemputb(to_dpt, DPT_SIZE, dpt_addr);
+
+   /* reset */
+   biosdisk(0, 0, 0, 0, 1, 0, NULL);
+}
+
+
+
+/* ExecCommand:
+ *  Exécute la commande spécifiée (via l'interruption 13h du BIOS).
+ */
+static int ExecCommand(int drive, int density, struct floppy_cmd *fd_cmd)
+{
+   int i;
+   int ret=0x10;
+
+   SetDiskParameters(density);
+
+   for (i=0; i<DISK_RETRY; i++) {
+      ret=biosdisk(fd_cmd->cmd, drive, fd_cmd->head, fd_cmd->track,
+                    fd_cmd->sector, fd_cmd->nsects, fd_cmd->buffer);
+
+      if (ret==0)  /* commande OK? */
+         break;
+
+      if ((i>1) && (ret==0x11)) { /* commande non OK mais corrigée par ctrl? */
+         ret=0;
+         break;
+      }
+
+      /* reset du lecteur */
+      biosdisk(0, 0, 0, 0, 1, 0, NULL);
+   }
+
+   switch (ret) {
+
+      case 0x02:  /* address mark not found */
+         return 0x04;   /* erreur sur l'adresse */
+
+      case 0x03:  /* disk write-protected */
+         return 0x01;   /* disk protégé en écriture */
+
+      case 0x04:  /* sector not found */
+      case 0x07:  /* drive parameter activity failed */
+      case 0x10:  /* data read (CRC or ECC) error */
+      case 0x0A:  /* bad sector flag detected */
+      case 0x0B:  /* bad track flag detected */
+         return 0x08;   /* erreur sur les données */
+
+      case 0x06:  /* floppy disk removed */
+      case 0x80:  /* disk timed out or failed to respond */
+         return 0x10;   /* lecteur non prêt */
+
+      default:
+         return 0;  /* OK */
+   }
+}
+
+
+
+/* FloppyReadSector:
+ *  Lit le secteur spécifié sur la disquette.
+ */
+int FloppyReadSector(int drive, int density, int track, int sector, int nsects, unsigned char data[])
+{
+   struct floppy_cmd fd_cmd;
+
+   fd_cmd.cmd    = 2;
+   fd_cmd.head   = drive%2;
+   fd_cmd.track  = track;
+   fd_cmd.sector = sector;
+   fd_cmd.nsects = nsects;
+   fd_cmd.buffer = data;
+
+   return ExecCommand(drive/2, density, &fd_cmd);
+}
+
+
+
+/* FloppyWriteSector:
+ *  Ecrit le secteur spécifié sur la disquette.
+ */
+int FloppyWriteSector(int drive, int density, int track, int sector, int nsects, const unsigned char data[])
+{
+   struct floppy_cmd fd_cmd;
+
+   fd_cmd.cmd    = 3;
+   fd_cmd.head   = drive%2;
+   fd_cmd.track  = track;
+   fd_cmd.sector = sector;
+   fd_cmd.nsects = nsects;
+   fd_cmd.buffer = (unsigned char *)data;
+
+   return ExecCommand(drive/2, density, &fd_cmd);
+}
+
+
+
+/* FloppyFormatTrack:
+ *  Formate la piste en utilisant la table des headers spécifiée.
+ */
+int FloppyFormatTrack(int drive, int density, int track, const unsigned char header_table[])
+{
+   struct floppy_cmd fd_cmd;
+   int format_type = 0;
+
+   /* sélection du format de formatage */
+   switch (drive_type[drive/2]) {
+
+      case 1:
+         /* 320/360 kb in 360 kb drive */
+         format_type = 1;
+         break;
+
+      case 2:
+         /* 320/360 kb in 1.2 Mb drive */
+         format_type = 2;
+         break;
+
+      case 3:
+      case 4:
+      case 5:
+      case 6:
+         /* 720 kb in 720 kb/1.44 Mb/2.88 Mb drive */
+         format_type = 4;
+         break;
+   }
+
+   biosdisk(23, drive/2, 0, 0, 1, format_type, NULL);
+
+   fd_cmd.cmd    = 5;
+   fd_cmd.head   = drive%2;
+   fd_cmd.track  = track;
+   fd_cmd.sector = 1;
+   fd_cmd.nsects = 16;
+   fd_cmd.buffer = (unsigned char *)header_table;
+
+   return ExecCommand(drive/2, density, &fd_cmd);
+}
+
+
+
+/* FloppyInit:
+ *  Initialise le module de lecture de disquettes.
+ */
+int FloppyInit(struct floppy_info *fi, int enable_write_support)
+{
+   __dpmi_regs r;
+   int i, num_drives = 0;
+
+   (void) enable_write_support;
+
+   for (i=0; i<2; i++) {
+      /* get drive parameters (int 13h, function 08h) */
+      r.h.ah = 0x08;
+      r.h.dl = i;
+
+      __dpmi_int(0x13, &r);
+
+      if (r.x.flags&1) {  /* CF set? */
+         drive_type[i] = 0;
+
+         fi->drive_type[2*i] = 0;
+         fi->drive_type[2*i+1] = 0;
+      }
+      else {
+         if (r.h.bl > 6) {
+            drive_type[i] = 0;
+
+            fi->drive_type[2*i] = 0;
+            fi->drive_type[2*i+1] = 0;
+         }
+         else {
+            drive_type[i] = r.h.bl;
+
+            fi->drive_type[2*i] = r.h.bl;
+            fi->drive_type[2*i+1] = 0;  /* face 1 unsupported */
+         }
+
+         num_drives++;
+      }
+   }
+
+   fi->num_drives = num_drives;
+   fi->fm_support = 0;
+   fi->write_support = 1;
+
+   return num_drives;
+}
+
+
+
+/* FloppyExit:
+ *  Met au repos le module de lecture de disquettes.
+ */
+void FloppyExit(void)
+{
+   if (dpt_addr) {
+      /* restaure les paramètres originaux */
+      _dosmemputb(pc_dpt, DPT_SIZE, dpt_addr);
+
+      /* reset */
+      biosdisk(0, 0, 0, 0, 1, 0, NULL);
+
+      dpt_addr = 0;
+   }
+}
+
Index: tools/sap/fd2sap.c
===================================================================
--- tools/sap/fd2sap.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/fd2sap.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,232 @@
+/*
+ *
+ *  FD2SAP conversion FD => SAP
+ *
+ */
+
+#include <stdio.h>
+#include <strings.h>
+
+
+#define NBSIDE 4
+#define NBTRACK 80
+#define NBSECT 16
+#define SECTSIZE 256
+
+unsigned char disk[NBSIDE][NBTRACK][NBSECT][SECTSIZE];
+
+#define SIDESIZE (NBTRACK*NBSECT*SECTSIZE)
+
+int LoadFD(char * name)
+{
+   FILE * f;
+
+   int sector,track,side;
+   int nb_side,size;
+
+   f=fopen(name,"rb");
+   if (f==NULL) return -1;
+   
+   fseek(f, 0L, SEEK_END);
+   size = ftell(f);
+   fseek(f, 0L, SEEK_SET);
+
+   if (size==SIDESIZE) {
+   	for (track=0;track<NBTRACK;track++)
+   	for (sector=0;sector<NBSECT;sector++)
+   	{
+       		fread(disk[0][track][sector],sizeof(char),256,f);
+    	}
+	return 1;
+   } else 
+   if (size==2*SIDESIZE) {
+	for (side=0;side<2;side++)
+   	for (track=0;track<NBTRACK;track++)
+   	for (sector=0;sector<NBSECT;sector++)
+   	{
+       		fread(disk[side][track][sector],sizeof(char),256,f);
+    	}
+	return 2;
+   }else
+   if (size==4*SIDESIZE) {
+	for (side=0;side<4;side++)
+   	for (track=0;track<NBTRACK;track++)
+   	for (sector=0;sector<NBSECT;sector++)
+   	{
+       		fread(disk[side][track][sector],sizeof(char),256,f);
+    	}
+	return 4;
+   }
+
+   fclose(f);
+   return 0;
+}
+
+/*
+  ecriture au format TDS
+*/
+void SaveTDS(char * name,int side)
+{
+   FILE * f;
+   char descript[0xA0];
+
+   int i,j;
+
+   /* lecture du descripteur TDS */
+   f=fopen(name,"wb");
+   if (f==NULL) return;
+
+   /* construction du header */
+   for(i=0;i<0xA0;i++)
+   {
+     descript[i]=0;
+     for(j=0;j<8;j++)
+     {
+       if (disk[side][20][1+(i*8)/SECTSIZE][(i*8)%SECTSIZE]!=0xFF)
+       descript[i]|=(1<<j);
+     }
+   }
+   fwrite(descript,sizeof(char),(size_t)0xA0,f);
+
+
+   for (i=0;i<0xA0;i++)
+   for (j=0;j<8;j++)
+   {
+     if (   ( descript[i]&(1<<j) )!=0   )
+     {
+       int SEC,TRK;
+       SEC=(i%2)*8+j;
+       TRK=i/2;
+
+       fwrite(disk[side][TRK][SEC],sizeof(char),(size_t)SECTSIZE,f);
+     }
+   }
+
+   fclose(f);
+}
+
+short crcpuk_temp;
+static const short magic_puk[]={
+
+   0x0000, 0x1081, 0x2102, 0x3183,
+   0x4204, 0x5285, 0x6306, 0x7387,
+   0x8408, 0x9489, 0xa50a, 0xb58b,
+   0xc60c, 0xd68d, 0xe70e, 0xf78f
+
+ };
+
+char * puk_header="\x01SYSTEME D'ARCHIVAGE PUKALL S.A.P. (c) Alexandre PUKALL Avril 1998";
+
+crc_pukall(short c)
+ {
+    register short puk;
+
+    /* on repete le code pour aller plus vite. Pas de boucle ici */
+
+    puk = (crcpuk_temp ^ c) & 0xf;
+    crcpuk_temp = ((crcpuk_temp>>4) & 0xfff) ^ magic_puk[puk];
+
+    c >>= 4;
+
+    puk = (crcpuk_temp ^ c) & 0xf;
+    crcpuk_temp = ((crcpuk_temp>>4) & 0xfff) ^ magic_puk[puk];
+ }
+
+
+void SaveSAP(char * name,int side)
+{
+
+	FILE *f;
+	unsigned char format,protection,piste,secteur;
+
+	char *buffer;
+	int fois;
+	format=0;
+	protection=0;  /* variables de test */
+
+	f=fopen(name,"wb");
+	if (f==NULL) return;
+
+/*
+ *	Ecriture du header
+ */
+
+	fwrite(puk_header,strlen(puk_header),sizeof(char),f);
+
+/*
+ *	Ecriture des donnees
+ */
+
+	for (piste=0;piste<80;piste++)
+	for (secteur=1;secteur<=16;secteur++)
+	{
+
+		crcpuk_temp = 0xffff; /* toujours remettre cette variable a 0xffff */
+			  /* pour calculer un nouveau CRC */
+
+	    	crc_pukall(format);   /* calcul du CRC d'un secteur */
+    		crc_pukall(protection);
+    		crc_pukall(piste);
+    		crc_pukall(secteur);
+
+		buffer=disk[side][piste][secteur-1];
+    		for (fois=0;fois<=255;fois++) /* on admet que buffer contient les */
+     		{                            /* donnees d'un secteur S.A.P. */
+       			crc_pukall(buffer[fois]);
+       			buffer[fois]=buffer[fois]^0xb3;
+	     	}
+
+		fputc(format & 0xFF,f);
+		fputc(protection & 0xFF,f);
+		fputc(piste & 0xFF,f);
+		fputc(secteur & 0xFF,f);
+
+		fwrite(buffer,256,sizeof(char),f);
+
+		fputc((crcpuk_temp >> 8) & 0xFF,f);
+		fputc(crcpuk_temp & 0xFF,f);
+	
+		
+	}	
+
+		
+
+	fclose(f);
+}
+
+
+int main(int argc,char * argv[])
+{
+	char str[1024];
+	char str2[1024];
+	char str3[32];
+
+	int i,nb_side;
+
+	if (argc!=3)
+	{
+		fprintf(stderr,"syntaxe : %s <fichier fd source> <fichier SAP dest>\n",argv[0]);
+		exit (-1);
+   	}
+
+   	nb_side=LoadFD(argv[1]);
+
+	if (nb_side<=0) {
+		fprintf(stderr,"Bad FD file");
+		exit(-1);
+	}
+
+	strcpy(str,argv[2]);
+
+	if (strlen(str)>4)
+	if (strcmp(&str[strlen(str)-4],".sap")==0) str[strlen(str)-4]='\0';
+
+	for (i=0;i<nb_side;i++) {
+		strcpy(str2,str);
+		strcat(str2,"_side");
+		sprintf(str3,"%d",i);
+		strcat(str2,str3);
+		strcat(str2,".sap");	
+   		SaveSAP(str2,i);
+	}  
+}
Index: tools/sap/floppy.h
===================================================================
--- tools/sap/floppy.h	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/floppy.h	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,47 @@
+/*  Lecture des disquettes Thomson sur PC
+ *  Version 2.1
+ *  Copyright (C) 2000-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#ifndef FLOPPY_H
+#define FLOPPY_H
+
+struct floppy_info {
+   int num_drives;
+   int drive_type[4];
+   int fm_support;
+   int write_support;
+};
+
+/* drive type: 1 (5"25 - 360 kb)
+ *             2 (5"25 - 1.2 Mb)
+ *             3 (3"5  - 720 kb)
+ *             4 (3"5 - 1.44 Mb)
+ *             5 (3"5 - 2.88 Mb)
+ *             6 (3"5 - 2.88 Mb)
+ */
+
+extern int FloppyInit(struct floppy_info *fi, int enable_write_support);
+extern void FloppyExit(void);
+
+extern int FloppyReadSector(int drive, int density, int track, int sector, int nsects, unsigned char data[]);
+extern int FloppyWriteSector(int drive, int density, int track, int sector, int nsects, const unsigned char data[]);
+extern int FloppyFormatTrack(int drive, int density, int track, const unsigned char header_table[]);
+
+#endif
+
Index: tools/sap/lfloppy.c
===================================================================
--- tools/sap/lfloppy.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/lfloppy.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,328 @@
+/*  Lecture des disquettes Thomson sous Linux
+ *  Version 2.1
+ *  Copyright (C) 2000-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#ifndef SCAN_DEPEND
+   #include <stdio.h>
+   #include <fcntl.h>
+   #include <unistd.h>
+   #include <sys/ioctl.h>
+   #include <linux/fd.h>
+   #include <linux/fdreg.h>
+#endif
+
+#include "floppy.h"
+
+
+#define DISK_RETRY   4
+#define RETRY2	10
+
+static int rw_flag;
+static int fd[2] = {-1, -1};
+static int drive_type[2];
+
+#define IS_5_INCHES(drive) ((drive_type[drive]>0) && (drive_type[drive]<3))
+
+#define SET_NO_MULTITRACK(lval)  (lval &= ~0x80)
+#define SET_NO_MFM(lval)  (lval &= ~0x40)
+
+
+
+/* ResetDrive:
+ *  Réinitialise le lecteur de disquettes.
+ */
+static void ResetDrive(int drive, int density)
+{
+   struct floppy_struct fd_prm;
+
+   fd_prm.head    = (density == 1 ? 1 : 2);  /* nr of heads */
+   fd_prm.track   = (IS_5_INCHES(drive) ? 40 : 80);  /* nr of tracks */
+   fd_prm.sect    = (density == 1 ? 4 : 8);  /* sectors per track (512-byte sector) */
+   fd_prm.size    = fd_prm.head * fd_prm.track * fd_prm.sect;  /* nr of sectors total */
+   fd_prm.stretch = 0;
+   fd_prm.gap     = 0x1B;
+   fd_prm.rate    = (density == 1 ? 0xB1 : 0x3A);  /* cf /usr/include/linux/fd.h */
+   fd_prm.spec1   = 0xDF;
+   fd_prm.fmt_gap = 0x2C;
+
+   ioctl(fd[drive], FDSETPRM, &fd_prm);
+}
+
+
+
+/* OpenDrive:
+ *  Obtient le descripteur de fichier pour le lecteur de disquettes.
+ */
+static int OpenDrive(int drive, int density)
+{
+   char dev_str[16];
+
+   snprintf(dev_str, sizeof(dev_str), "/dev/fd%d", drive);
+
+   if (rw_flag) {
+      if ((fd[drive]=open(dev_str, O_RDWR | O_NDELAY))>=0)
+         goto success;
+   }
+   else {
+      if ((fd[drive]=open(dev_str, O_RDONLY | O_NDELAY))>=0)
+         goto success;
+   }
+
+   return 0;
+
+  success:
+    ResetDrive(drive, density);
+    return 1;
+}
+
+
+
+/* ExecCommand:
+ *  Exécute la commande spécifiée via l'appel ioctl() FDRAWCMD.
+ */
+static int ExecCommand2(int drive, int density, struct floppy_raw_cmd *fd_cmd);
+
+static int ExecCommand(int drive,int density, struct floppy_raw_cmd *fd_cmd) {
+ int i,ret;
+ for (i=0;i<RETRY2;i++) {
+   if ((ret=ExecCommand2(drive,density,fd_cmd))==0) return 0;
+   ResetDrive(drive,density);
+}
+return ret;
+}
+
+static int ExecCommand2(int drive, int density, struct floppy_raw_cmd *fd_cmd)
+{
+   int i, ret = 0;
+
+   if (fd[drive]<0 && !OpenDrive(drive, density))
+      return 0x10;  /* lecteur non prêt */
+
+   for (i=0; i<DISK_RETRY; i++) {
+      ret=ioctl(fd[drive], FDRAWCMD, fd_cmd);
+
+      if (ret >= 0)
+         break;
+
+      ResetDrive(drive, density);
+   }
+
+   if (ret<0)
+      return 0x10;  /* lecteur non prêt */
+
+#ifdef DEBUG
+   printf("fd_cmd reply: ");
+   for (i=0; i<3; i++)
+      printf("ST%d=%02x ", i, fd_cmd->reply[i]);
+   printf("\n");
+#endif
+
+   switch (fd_cmd->reply[1]) { /* ST1 */
+
+      case 0x01:  /* Missing Address Mark */
+         return 0x04;   /* erreur sur l'adresse */
+
+      case 0x02:  /* Write Protected */
+         return 0x01;   /* disk protégé en écriture */
+
+      case 0x04:  /* No Data - unreadable */
+         return 0x08;   /* erreur sur les données */
+
+      case 0x20:  /* CRC error in data or addr */
+         if (fd_cmd->reply[2]==0x20)
+            return 0x08;   /* erreur sur les données */
+         else
+            return 0x04;   /* erreur sur l'adresse */
+
+      default:
+         return 0;  /* OK */
+   }
+}
+
+
+
+/* FloppyReadSector:
+ *  Lit le secteur spécifié sur la disquette.
+ */
+int FloppyReadSector(int drive, int density, int track, int sector, int nsects, unsigned char data[])
+{
+   struct floppy_raw_cmd fd_cmd;
+   int pc_drive = drive/2;
+
+   /* paramètres de commande */
+   fd_cmd.flags  = FD_RAW_READ | FD_RAW_INTR | FD_RAW_NEED_SEEK;
+   fd_cmd.data   = data;
+   fd_cmd.length = (density == 1 ? 128 : 256)*nsects;  /* buffer length */
+   fd_cmd.rate   = IS_5_INCHES(pc_drive) ? 1 : 2;
+   fd_cmd.track  = IS_5_INCHES(pc_drive) ? track*2 : track;  /* physical cylinder */
+
+   fd_cmd.cmd[0] = FD_READ;
+   fd_cmd.cmd[1] = (drive%2) << 2;  /* physical head */
+   fd_cmd.cmd[2] = track;  /* logical cylinder */
+   fd_cmd.cmd[3] = 0;  /* logical head */
+   fd_cmd.cmd[4] = sector;
+   fd_cmd.cmd[5] = density - 1;  /* sector size = 128*2^n */
+   fd_cmd.cmd[6] = 16;  /* num sectors */
+   fd_cmd.cmd[7] = 0x1B;  /* gap */
+   fd_cmd.cmd[8] = 0xFF;
+   fd_cmd.cmd_count = 9;
+
+   SET_NO_MULTITRACK(fd_cmd.cmd[0]);
+
+   if (density == 1)
+      SET_NO_MFM(fd_cmd.cmd[0]);  /* FM coding */
+
+   return ExecCommand(pc_drive, density, &fd_cmd);
+}
+
+
+
+/* FloppyWriteSector:
+ *  Ecrit le secteur spécifié sur la disquette.
+ */
+int FloppyWriteSector(int drive, int density, int track, int sector, int nsects, const unsigned char data[])
+{
+   struct floppy_raw_cmd fd_cmd;
+   int pc_drive = drive/2;
+
+   /* paramètres de commande */
+   fd_cmd.flags  = FD_RAW_WRITE | FD_RAW_INTR | FD_RAW_NEED_SEEK;
+   fd_cmd.data   = (unsigned char *)data;
+   fd_cmd.length = (density == 1 ? 128 : 256)*nsects;  /* buffer length */
+   fd_cmd.rate   = IS_5_INCHES(pc_drive) ? 1 : 2;
+   fd_cmd.track  = IS_5_INCHES(pc_drive) ? track*2 : track;  /* physical cylinder */
+
+   fd_cmd.cmd[0] = FD_WRITE;
+   fd_cmd.cmd[1] = (drive%2) << 2;  /* physical head */
+   fd_cmd.cmd[2] = track;  /* logical cylinder */
+   fd_cmd.cmd[3] = 0;  /* logical head */
+   fd_cmd.cmd[4] = sector;
+   fd_cmd.cmd[5] = density - 1;  /* sector size = 128*2^n */
+   fd_cmd.cmd[6] = 16;  /* num sectors */
+   fd_cmd.cmd[7] = 0x1B;  /* gap */
+   fd_cmd.cmd[8] = 0xFF;
+   fd_cmd.cmd_count = 9;
+
+   SET_NO_MULTITRACK(fd_cmd.cmd[0]);
+
+   if (density == 1)
+      SET_NO_MFM(fd_cmd.cmd[0]);  /* FM coding */
+
+   return ExecCommand(pc_drive, density, &fd_cmd);
+}
+
+
+
+/* FloppyFormatTrack:
+ *  Formate la piste en utilisant la table des headers spécifiée.
+ */
+int FloppyFormatTrack(int drive, int density, int track, const unsigned char header_table[])
+{
+   struct floppy_raw_cmd fd_cmd;
+   int pc_drive = drive/2;
+
+   /* paramètres de commande */
+   fd_cmd.flags  = FD_RAW_WRITE | FD_RAW_INTR | FD_RAW_NEED_SEEK;
+   fd_cmd.data   = (unsigned char *)header_table;
+   fd_cmd.length = 64;
+   fd_cmd.rate   = IS_5_INCHES(pc_drive) ? 1 : 2;
+   fd_cmd.track  = IS_5_INCHES(pc_drive) ? track*2 : track;  /* physical cylinder */
+
+   fd_cmd.cmd[0] = FD_FORMAT;
+   fd_cmd.cmd[1] = (drive%2) << 2;
+   fd_cmd.cmd[2] = density - 1;  /* sector size = 128*2^n */
+   fd_cmd.cmd[3] = 16;  /* num sectors */
+   fd_cmd.cmd[4] = 0x2C;
+   fd_cmd.cmd[5] = 0xE5;  /* filler_byte */
+   fd_cmd.cmd_count = 6;
+
+   SET_NO_MULTITRACK(fd_cmd.cmd[0]);  /* nop */
+
+   if (density == 1)
+      SET_NO_MFM(fd_cmd.cmd[0]);  /* FM coding */
+
+   return ExecCommand(pc_drive, density, &fd_cmd);
+}
+
+
+
+/* FloppyInit:
+ *  Initialise le module de lecture de disquettes.
+ */
+int FloppyInit(struct floppy_info *fi, int enable_write_support)
+{
+   struct floppy_drive_params fd_params;
+   char dev_str[16];
+   int i, num_drives = 0;
+
+   for (i=0; i<2; i++) {
+      /* get drive type */
+      snprintf(dev_str, sizeof(dev_str), "/dev/fd%d", i);
+
+      if ((fd[i]=open(dev_str, O_RDONLY | O_NDELAY))<0) {
+         drive_type[i] = 0;
+         fi->drive_type[2*i] = 0;
+         fi->drive_type[2*i+1] = 0;
+      }
+      else {
+         ioctl(fd[i], FDGETDRVPRM, &fd_params);
+         close(fd[i]);
+         fd[i] = -1;
+
+         if (fd_params.cmos > 6) {
+            drive_type[i] = 0;
+            fi->drive_type[2*i] = 0;
+            fi->drive_type[2*i+1] = 0;
+         }
+         else {
+            drive_type[i] = fd_params.cmos;
+            fi->drive_type[2*i] = fd_params.cmos;
+            fi->drive_type[2*i+1] = fd_params.cmos;
+         }
+
+         num_drives++;
+      }
+   }
+
+   fi->num_drives = num_drives;
+   fi->fm_support = 1;
+   fi->write_support = enable_write_support;
+
+   rw_flag = enable_write_support;
+   
+   return num_drives;
+}
+
+
+
+/* FloppyExit:
+ *  Met au repos le module de lecture de disquettes.
+ */
+void FloppyExit(void)
+{
+   int i;
+
+   for (i=0; i<2; i++) {
+      if (fd[i] >= 0) {
+         close(fd[i]);
+         fd[i] = -1;
+      }
+   }
+}
+
Index: tools/sap/libsap.c
===================================================================
--- tools/sap/libsap.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/libsap.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,1801 @@
+/*  LibSAP
+ *  Version 0.9.4
+ *  Copyright (C) 2000-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "libsap.h"
+
+
+int sap_errno;
+
+
+#define FILENAME_LENGTH 128
+#define MAX_ARCHIVE       8
+
+#define SAP_MAGIC_NUM  0xB3
+#define SAP_HEADER_SIZE  66
+static const char sap_header[]="\0SYSTEME D'ARCHIVAGE PUKALL S.A.P. "
+                               "(c) Alexandre PUKALL Avril 1998";
+
+
+static const int sap_format_table[][3] = {
+   { SAP_NTRACKS1, SAP_SECTSIZE1, SAP_TRACKSIZE1 },
+   { SAP_NTRACKS2, SAP_SECTSIZE2, SAP_TRACKSIZE2 }
+};
+
+#define SAP_NTRACKS(f)      sap_format_table[f-SAP_FORMAT1][0]
+#define SAP_SECTSIZE(f)     sap_format_table[f-SAP_FORMAT1][1]
+#define SAP_TRACKSIZE(f)    sap_format_table[f-SAP_FORMAT1][2]
+
+#define SAP_EXTSECTSIZE(f)  (SAP_SECTSIZE(f) + sizeof(sapsector_t) - SAP_SECTSIZE1)
+
+
+static short int crcpuk_temp;
+
+static short int puktable[]={
+   0x0000, 0x1081, 0x2102, 0x3183,
+   0x4204, 0x5285, 0x6306, 0x7387,
+   0x8408, 0x9489, 0xa50a, 0xb58b,
+   0xc60c, 0xd68d, 0xe70e, 0xf78f
+};
+
+
+#define NO_ARCHIVE     0
+#define EMPTY_ARCHIVE  1
+#define FILLED_ARCHIVE 2
+#define FULL_ARCHIVE   3
+
+static struct {
+   int format;
+   int state;
+   int ntracks;
+   FILE *file;
+} archive[MAX_ARCHIVE+1];
+
+#define ID_FORMAT(id)   archive[id].format
+#define ID_STATE(id)    archive[id].state
+#define ID_NTRACKS(id)  archive[id].ntracks
+#define ID_FILE(id)     archive[id].file
+
+
+/* constants for the TO logical disk format */
+#define TO_NSECTS                16
+#define TO_SECTOR_PER_BLOCK      8
+
+#define TO_SECTSIZE1             255  /* not 256 */
+#define TO_FAT_START1            257
+#define TO_NBLOCKS1              160
+#define TO_BLOCKSIZE1            (TO_SECTOR_PER_BLOCK*TO_SECTSIZE1)
+#define TO_FILESIZE_MAX1         (TO_NBLOCKS1-2)
+#define TO_DIR_START1            512
+
+#define TO_SECTSIZE2             127  /* not 128 */
+#define TO_FAT_START2            129
+#define TO_NBLOCKS2              80
+#define TO_BLOCKSIZE2            (TO_SECTOR_PER_BLOCK*TO_SECTSIZE2)
+#define TO_FILESIZE_MAX2         (TO_NBLOCKS2-2)
+#define TO_DIR_START2            256
+
+#define TO_FILLER_BYTE           0xE5
+#define TO_TAG_RESERVED          0xFE
+#define TO_TAG_FREE              0xFF
+#define TO_END_BLOCK_OFFSET      0xC0
+
+#define TO_DIRENTRY_LENGTH       32
+#define TO_NAME                  0
+#define TO_NAME_LENGTH           8
+#define TO_EXT                   8
+#define TO_EXT_LENGTH            3
+#define TO_FILE_TYPE             11
+#define TO_DATA_TYPE             12
+#define TO_FIRST_BLOCK           13
+#define TO_END_SIZE              14
+#define TO_COMMENT               16
+#define TO_COMMENT_LENGTH        8
+#define TO_DATE_DAY              24
+#define TO_DATE_MONTH            25
+#define TO_DATE_YEAR             26
+#define TO_CHG_MODE              30
+#define TO_CHG_CHECKSUM          31
+
+#define TO_DIRENTRY_PER_SECTOR1  8
+#define TO_NDIRENTRIES1          (TO_NSECTS-2)*TO_DIRENTRY_PER_SECTOR1
+
+#define TO_DIRENTRY_PER_SECTOR2  4
+#define TO_NDIRENTRIES2          (TO_NSECTS-2)*TO_DIRENTRY_PER_SECTOR2
+
+static const int to_format_table[][8] = {
+   { TO_SECTSIZE1, TO_FAT_START1, TO_NBLOCKS1, TO_BLOCKSIZE1,
+     TO_FILESIZE_MAX1, TO_DIR_START1, TO_DIRENTRY_PER_SECTOR1, TO_NDIRENTRIES1 },
+   { TO_SECTSIZE2, TO_FAT_START2, TO_NBLOCKS2, TO_BLOCKSIZE2,
+     TO_FILESIZE_MAX2, TO_DIR_START2, TO_DIRENTRY_PER_SECTOR2, TO_NDIRENTRIES2 }
+};
+
+#define TO_SECTSIZE(f)             to_format_table[f-SAP_FORMAT1][0]
+#define TO_FAT_START(f)            to_format_table[f-SAP_FORMAT1][1]
+#define TO_NBLOCKS(f)              to_format_table[f-SAP_FORMAT1][2]
+#define TO_BLOCKSIZE(f)            to_format_table[f-SAP_FORMAT1][3]
+#define TO_FILESIZE_MAX(f)         to_format_table[f-SAP_FORMAT1][4]
+#define TO_DIR_START(f)            to_format_table[f-SAP_FORMAT1][5]
+#define TO_DIRENTRY_PER_SECTOR(f)  to_format_table[f-SAP_FORMAT1][6]
+#define TO_NDIRENTRIES(f)          to_format_table[f-SAP_FORMAT1][7]
+
+
+/* constants for the TO memory layout */
+#define TO_BANKSIZE             16384
+
+
+/* file types */
+enum {
+   FTYPE_BASIC,
+   FTYPE_DATA,
+   FTYPE_MACHINE,
+   FTYPE_ASM,
+   FTYPE_OTHERS,
+   FTYPE_PARAGRAPHE = 0xA
+};
+
+static char ftype_sym[5] = { 'B', 'D', 'M', 'A', '*' };
+
+enum {
+   DTYPE_BINARY,
+   DTYPE_ASCII = 0xFF,
+   DTYPE_OTHERS = 2
+};
+
+static char dtype_sym[3] = { 'B', 'A', '*' };
+
+struct extension_t {
+   char tag[TO_EXT_LENGTH+1];
+   unsigned char ftype;
+   unsigned char dtype;
+};
+
+enum {
+   EXT_TYPE_BAS,
+   EXT_TYPE_DAT,
+   EXT_TYPE_ASC,
+   EXT_TYPE_ASM,
+   EXT_TYPE_BIN,
+   EXT_TYPE_MAP,
+   EXT_TYPE_CHG,
+   EXT_TYPE_CFG,
+   EXT_TYPE_BAT,
+   EXT_TYPE_CAR,
+   EXT_TYPE_PAR,
+   EXT_TYPE_UNKNOWN,
+   EXT_TYPE_MAX
+};
+
+static struct extension_t ext_type[EXT_TYPE_MAX] = {
+   { "BAS", FTYPE_BASIC,      DTYPE_BINARY },
+   { "DAT", FTYPE_DATA,       DTYPE_ASCII  },
+   { "ASC", FTYPE_DATA,       DTYPE_ASCII  },
+   { "ASM", FTYPE_ASM,        DTYPE_ASCII  },
+   { "BIN", FTYPE_MACHINE,    DTYPE_BINARY },
+   { "MAP", FTYPE_MACHINE,    DTYPE_BINARY },
+   { "CHG", FTYPE_DATA,       DTYPE_ASCII  },
+   { "CFG", FTYPE_MACHINE,    DTYPE_BINARY },
+   { "BAT", FTYPE_BASIC,      DTYPE_BINARY },
+   { "CAR", FTYPE_DATA,       DTYPE_ASCII  },
+   { "PAR", FTYPE_PARAGRAPHE, DTYPE_BINARY },
+   { "???", FTYPE_MACHINE,    DTYPE_BINARY }
+};
+
+
+#define MIN(x,y)     (((x) < (y)) ? (x) : (y))
+
+
+
+/************************************************/
+/***    private functions: helper routines    ***/
+/************************************************/
+
+
+/* get_id:
+ *  Finds the first empty id, returns 0 if has failed.
+ */
+static int get_id(void)
+{
+   int i;
+
+   for (i=MAX_ARCHIVE; i>0; i--) {
+      if (archive[i].state == NO_ARCHIVE)
+         break;
+   }
+
+   return i;
+}
+
+
+
+/* clean_string:
+ *  Helper function to clean up character strings.
+ */
+static inline void clean_string(unsigned char *str)
+{
+   while (*str) {
+      if ((*str<32) || (*str>127))
+         *str = '#';
+
+      str++;
+   }
+}
+
+
+
+/************************************************/
+/***   private functions: SAP format support  ***/
+/************************************************/
+
+
+/* crc_pukall:
+ *  Computes the new CRC from the c data.
+ */
+static void crc_pukall(short int c)
+{
+   short int index;
+
+   index = (crcpuk_temp ^ c) & 0xf;
+   crcpuk_temp = ((crcpuk_temp>>4) & 0xfff) ^ puktable[index];
+
+   c >>= 4;
+
+   index = (crcpuk_temp ^ c) & 0xf;
+   crcpuk_temp = ((crcpuk_temp>>4) & 0xfff) ^ puktable[index];
+}
+
+
+
+/* do_crc:
+ *  Computes the CRC for the specified SAP sector.
+ */
+static void do_crc(sapsector_t *sapsector, int format)
+{
+   int i;
+
+   crcpuk_temp = 0xffff;
+
+   crc_pukall(sapsector->format);
+   crc_pukall(sapsector->protection);
+   crc_pukall(sapsector->track);
+   crc_pukall(sapsector->sector);
+
+   for (i=0; i<SAP_SECTSIZE(format); i++)
+      crc_pukall(sapsector->data[i]);
+}
+
+
+
+/* do_read_sector: 
+ *  Performs the low-level read operation for the specified sector.
+ */
+static void do_read_sector(sapID id, sapsector_t *sapsector)
+{
+   unsigned char buffer[sizeof(sapsector_t)];
+   int format = ID_FORMAT(id);
+   int i;
+
+   i=fread(buffer, sizeof(char), SAP_EXTSECTSIZE(format), ID_FILE(id));
+
+   sapsector->format     = buffer[0];
+   sapsector->protection = buffer[1];
+   sapsector->track      = buffer[2];
+   sapsector->sector     = buffer[3];
+
+   for (i=0; i<SAP_SECTSIZE(format); i++)
+       sapsector->data[i] = buffer[4+i]^SAP_MAGIC_NUM;
+
+   sapsector->data[SAP_SECTSIZE(format)] = buffer[4+i];
+   sapsector->data[SAP_SECTSIZE(format)+1] = buffer[4+i+1];
+}
+
+
+
+/* do_write_sector:
+ *  Performs the low-level write operation for the specified sector.
+ */
+static void do_write_sector(sapID id, sapsector_t *sapsector)
+{
+   unsigned char buffer[sizeof(sapsector_t)];
+   int format = ID_FORMAT(id);
+   int i;
+
+   /* fill in the write buffer */
+   buffer[0] = sapsector->format;
+   buffer[1] = sapsector->protection;
+   buffer[2] = sapsector->track;
+   buffer[3] = sapsector->sector;
+
+   for (i=0; i<SAP_SECTSIZE(format); i++)
+      buffer[4+i] = sapsector->data[i]^SAP_MAGIC_NUM;
+
+   /* compute the CRC */
+   do_crc(sapsector, format);
+   buffer[4+i] = sapsector->data[SAP_SECTSIZE(format)] = (crcpuk_temp>>8)&0xff;
+   buffer[4+i+1] = sapsector->data[SAP_SECTSIZE(format)+1] = crcpuk_temp&0xff;
+
+   fwrite(buffer, sizeof(char), SAP_EXTSECTSIZE(format), ID_FILE(id));
+}
+
+
+
+/* seek_pos:
+ *  Seeks the specified position in the SAP archive.
+ */
+static void seek_pos(sapID id, int track, int sect)
+{
+   int pos;
+
+   pos = SAP_HEADER_SIZE + (track*SAP_NSECTS + sect-1) * SAP_EXTSECTSIZE(ID_FORMAT(id));
+
+   fseek(ID_FILE(id), pos, SEEK_SET);
+}
+
+
+
+/************************************************/
+/***  private functions: Thomson DOS support  ***/
+/************************************************/
+
+
+
+/* get_file_size:
+ *  Returns the size in blocks of the specified file on disk.
+ */
+static int get_file_size(int format, int n, const unsigned char trk20_data[])
+{
+   const unsigned char *fat_data, *entry_data;
+   int block, size = 0;
+
+   fat_data = trk20_data + TO_FAT_START(format);
+   entry_data = trk20_data + TO_DIR_START(format) + n*TO_DIRENTRY_LENGTH;
+
+   block = entry_data[TO_FIRST_BLOCK];
+
+   while ((block<TO_NBLOCKS(format)) && (size<TO_FILESIZE_MAX(format))) {
+      size++;
+      block = fat_data[block];
+   }
+
+   return size;
+}
+
+
+
+/* get_dskf:
+ *  Returns the amount of free space in blocks on disk.
+ */
+static int get_dskf(int format, const unsigned char trk20_data[])
+{
+   const unsigned char *fat_data;
+   int i, dskf = 0;
+
+   fat_data = trk20_data + TO_FAT_START(format);
+
+   for (i=0; i<TO_NBLOCKS(format); i++) {
+      if (fat_data[i] == TO_TAG_FREE)
+         dskf++;
+   }
+
+   return dskf;
+}
+
+
+
+/* extract_dir_entry:
+ *  Extracts one dir entry and returns the number of characters.
+ */
+static int extract_dir_entry(int format, char buffer[], int buffer_size, int n, const unsigned char trk20_data[])
+{
+   const unsigned char *entry_data;
+   char name[TO_NAME_LENGTH+1], ext[TO_EXT_LENGTH+1], date[9], *comment;
+   unsigned char ftype, dtype;
+   int i, len, size;
+
+   entry_data = trk20_data + TO_DIR_START(format) + n*TO_DIRENTRY_LENGTH;
+
+   comment = malloc(TO_COMMENT_LENGTH+1);
+
+   if ((entry_data[TO_NAME] == 0) || (entry_data[TO_NAME] == TO_TAG_FREE))
+      return 0;
+
+   if (entry_data[TO_NAME] == 1) {
+      /* display only comment (if any) */
+      if (entry_data[TO_COMMENT]) {
+         comment[0] = '\0';
+         strncat(comment,(char*)(entry_data+TO_COMMENT), TO_COMMENT_LENGTH);
+
+         for (i=1; i<TO_COMMENT_LENGTH; i++) {
+            if (comment[i] == 18) { /* DC2 (repetition) ? */
+               len = strlen(comment);
+               comment = realloc(comment, len + comment[i+1]);
+               memmove(comment+i+comment[i+1], comment+i+2, len - (i+2) + 1);
+               memset(comment+i, comment[i-1], comment[i+1]);
+            }
+         }
+
+         return sprintf(buffer, "%30s%s\n", "", comment);
+      }
+      else {
+         return 0;
+      }
+   }
+
+   /* name */
+   name[0] = 0;
+   strncat(name,(char*)(entry_data+TO_NAME), TO_NAME_LENGTH);
+   clean_string((unsigned char*)name);
+
+   /* extension */
+   ext[0] = 0;
+   strncat(ext,(char*)(entry_data+TO_EXT), TO_EXT_LENGTH);
+   clean_string((unsigned char *)ext);
+
+   /* file type */
+   ftype = entry_data[TO_FILE_TYPE];
+   ftype = (ftype > 3 ? ftype_sym[4] : ftype_sym[ftype]);
+
+   /* data type */
+   dtype = entry_data[TO_DATA_TYPE];
+   dtype = (dtype == 0 ? dtype_sym[0] : (dtype == 0xFF ? dtype_sym[1] : dtype_sym[2]));
+
+   /* size */
+   size = get_file_size(format, n, trk20_data) * ((TO_BLOCKSIZE(format) + 1024) / 1024);
+
+   /* date */
+   if ((entry_data[TO_DATE_DAY] >= 1) && (entry_data[TO_DATE_DAY] <= 31) &&
+       (entry_data[TO_DATE_MONTH] >= 1) && (entry_data[TO_DATE_MONTH] <= 12) &&
+       (entry_data[TO_DATE_YEAR] <= 99))
+      sprintf(date, "%2d-%2d-%2d", entry_data[TO_DATE_DAY], entry_data[TO_DATE_MONTH], entry_data[TO_DATE_YEAR]);
+   else
+      date[0] = 0;
+
+   /* comment */
+   comment[0] = 0;
+   if (entry_data[TO_COMMENT])
+      strncat(comment,(char*)(entry_data+TO_COMMENT), TO_COMMENT_LENGTH);
+
+   /* display the entry */
+   i = sprintf(buffer, "%-8s %-3s %c %c %-3d %-8s %s\n", name, ext, ftype, dtype, size, date, comment);
+   free(comment);
+
+   return i;
+
+   (void) buffer_size;  /* DJGPP 2.03 lacks snprintf() */
+}
+
+
+
+/* get_filename:
+ *  Detects whether the file entry is valid and returns the filename if so.
+ */
+static int get_filename(char filename[], const unsigned char entry_data[])
+{
+   int i, j;
+
+   if ((entry_data[TO_NAME] == 0) || (entry_data[TO_NAME] == TO_TAG_FREE))
+      return -1;
+
+   /* name */
+   strncpy(filename,(char*)(entry_data+TO_NAME), TO_NAME_LENGTH);
+
+   for (i=0; i<TO_NAME_LENGTH; i++) {
+      if (filename[i] == 32)
+         break;
+   }
+
+   filename[i++] = '.';
+   filename[i] = 0;
+
+   /* extension */
+   strncat(filename,(char*)(entry_data+TO_EXT), TO_EXT_LENGTH);
+
+   for (j=0; j<TO_EXT_LENGTH; j++) {
+      if (filename[i+j] == 32)
+         break;
+   }
+
+   if (filename[i+j-1] == '.')
+      filename[i+j-1] = 0;
+   else
+      filename[i+j] = 0;
+
+   return 0;
+}
+
+
+
+/* decode_filename:
+ *  Decodes the specified raw filename into a valid dir entry.
+ */
+static void decode_filename(unsigned char entry_data[], const char filename[], int file_size)
+{
+   const char *p;
+   char shortname[128];
+   char ext[TO_EXT_LENGTH+1];
+   int i, type, len, ext_len = 0;
+   FILE *file;
+
+   /* zero the entry data */
+   memset(entry_data + TO_NAME, 32, TO_NAME_LENGTH + TO_EXT_LENGTH);
+   entry_data[TO_COMMENT] = 0;
+   memset(entry_data + TO_COMMENT+1, 32, TO_COMMENT_LENGTH-1);
+   memset(entry_data + TO_DATE_DAY, 0, TO_DIRENTRY_LENGTH - TO_DATE_DAY);
+
+   /* find short name */
+   len = strlen(filename);
+   p = filename + len - 1;   
+
+   while ((p>=filename) && (*p != '/'))
+      p--;
+
+   shortname[0] = 0;
+   strncat(shortname, p+1, sizeof(shortname) - 1);
+
+   /* find extension */
+   len = strlen(shortname);
+   p = shortname + len - 1;
+
+   while ((p>=shortname) && (*p != '.'))
+      p--;
+
+   if (p < shortname) {  /* no extension? */
+      memcpy(entry_data+TO_NAME, shortname, MIN(TO_NAME_LENGTH, len));
+      type = EXT_TYPE_MAX-1;
+   }
+   else {
+      /* name */
+      memcpy(entry_data+TO_NAME, shortname, MIN(TO_NAME_LENGTH, p - shortname));
+
+      /* extension */
+      ext_len = MIN(TO_EXT_LENGTH, shortname + len - 1 - p);
+      memcpy(entry_data+TO_EXT, p+1, ext_len);
+
+      /* build upper case extension */
+      ext[0] = 0;
+      strncat(ext, p+1, ext_len);
+      for (i=0; i<ext_len; i++)
+         ext[i] = toupper(ext[i]);
+
+      /* search for a standard extension */
+      for (type=0; type<EXT_TYPE_MAX-1; type++) {
+         if (strcmp(ext_type[type].tag, ext) == 0)
+            break;
+      }
+   }
+
+   /* set file and data types */
+   entry_data[TO_FILE_TYPE] = ext_type[type].ftype;
+   entry_data[TO_DATA_TYPE] = ext_type[type].dtype;
+
+   /* special treatment */
+   switch (type) {
+
+      case EXT_TYPE_BAS:
+         /* differentiate ASCII from BINARY data */
+         file = fopen(filename, "rb");
+         if (file) {
+            if ((fgetc(file) == '\r') && (fgetc(file) == '\n'))
+               entry_data[TO_DATA_TYPE] = DTYPE_ASCII;
+            fclose(file);
+         }
+         break;
+
+      case EXT_TYPE_CHG:
+         /* automatic launch + number of requested banks */
+         entry_data[TO_CHG_MODE] = 0x80 | ((file_size/TO_BANKSIZE)&0x3F);
+
+         /* 8-bit checksum */
+         for (i=0; i<TO_NAME_LENGTH; i++)
+            entry_data[TO_CHG_CHECKSUM] += entry_data[i];
+         break;
+   }
+}
+
+
+
+/* seek_file:
+ *  Seeks a file in the specified directory and returns its index number if found.
+ */
+static int seek_file(int format, const char filename[], const unsigned char dir_data[])
+{
+   char entry_name[TO_NAME_LENGTH + 1 + TO_EXT_LENGTH + 1];
+   int i;
+
+   for (i=0; i<TO_NDIRENTRIES(format); i++) {
+      if ((get_filename(entry_name, dir_data+i*TO_DIRENTRY_LENGTH) == 0) && (strcmp(entry_name, filename) == 0))
+         return i;
+   }
+
+   return -1;
+}
+
+
+
+/* wildcard handling code by Michael Bulkin (M.A.Bukin@inp.nsk.su) */
+#define WCD_MATCH_TRY 0
+#define WCD_MATCH_ONE 1
+#define WCD_MATCH_ANY 2
+
+
+struct WCD_MATCH_DATA
+{
+   int type;
+   const char *s1;
+   const char *s2;
+};
+
+
+
+/* wcdcmp:
+ *  Compares two strings ('*' matches any number of characters,
+ *  '?' matches any character).
+ */
+static int wcdcmp(const char *s1, const char *s2)
+{
+   static unsigned int size = 0;
+   static struct WCD_MATCH_DATA *data = NULL;
+   const char *s1end;
+   int index, c1, c2;
+
+   /* handle NULL arguments */
+   if ((!s1) && (!s2)) {
+      if (data) {
+         free(data);
+         data = NULL;
+      }
+
+      return 1;
+   }
+
+   s1end = s1 + strlen(s1);
+
+   /* allocate larger working area if necessary */
+   if (data && (size < strlen(s2))) {
+      free(data);
+      data = NULL;
+   }
+
+   if (!data) {
+      size = strlen(s2);
+      data = malloc(sizeof(struct WCD_MATCH_DATA) * size * 2 + 1);
+      if (!data)
+         return 1;
+   }
+
+   index = 0;
+   data[0].s1 = s1;
+   data[0].s2 = s2;
+   data[0].type = WCD_MATCH_TRY;
+
+   while (index >= 0) {
+      s1 = data[index].s1;
+      s2 = data[index].s2;
+      c1 = *s1;
+      c2 = *s2;
+
+      switch (data[index].type) {
+
+      case WCD_MATCH_TRY:
+         if (c2 == 0) {
+            /* pattern exhausted */
+            if (c1 == 0)
+               return 0;
+            else
+               index--;
+         }
+         else if (c1 == 0) {
+            /* string exhausted */
+            while (*s2 == '*')
+               s2++;
+            if (*s2 == 0)
+               return 0;
+            else
+               index--;
+         }
+         else if (c2 == '*') {
+            /* try to match the rest of pattern with empty string */
+            data[index++].type = WCD_MATCH_ANY;
+            data[index].s1 = s1end;
+            data[index].s2 = s2 + 1;
+            data[index].type = WCD_MATCH_TRY;
+         }
+         else if ((c2 == '?') || (c1 == c2)) {
+            /* try to match the rest */
+            data[index++].type = WCD_MATCH_ONE;
+            data[index].s1 = s1 + 1;
+            data[index].s2 = s2 + 1;
+            data[index].type = WCD_MATCH_TRY;
+         }
+         else
+            index--;
+         break;
+
+      case WCD_MATCH_ONE:
+         /* the rest of string did not match, try earlier */
+         index--;
+         break;
+
+      case WCD_MATCH_ANY:
+         /* rest of string did not match, try add more chars to string tail */
+         if (--data[index + 1].s1 >= s1) {
+            data[index + 1].type = WCD_MATCH_TRY;
+            index++;
+         }
+         else
+            index--;
+         break;
+
+      default:
+         /* this is a bird? This is a plane? No it's a bug!!! */
+         return 1;
+      }
+   }
+
+   return 1;
+}
+
+
+
+/* seek_wilcard:
+ *  Seeks a wildcard in the specified directory and returns its index number if found.
+ */
+static int seek_wildcard(int format, char filename[], const char pattern[], const unsigned char dir_data[])
+{
+   char entry_name[TO_NAME_LENGTH + 1 + TO_EXT_LENGTH + 1];
+   static int i = 0;
+   int j;
+
+   while (i<TO_NDIRENTRIES(format)) {
+      j = i++;
+
+      if ((get_filename(entry_name, dir_data+j*TO_DIRENTRY_LENGTH) == 0) && (wcdcmp(entry_name, pattern) == 0)) {
+         strcpy(filename, entry_name);
+         return j;
+      }
+   }
+
+   i = 0;
+
+   /* to avoid leaking memory */
+   wcdcmp(NULL, NULL);
+
+   return -1;
+}
+
+
+
+/* find_free_block_sym:
+ *  Finds a free block using a symmetrical search.
+ */
+static int find_free_block_sym(int format, unsigned char fat_data[])
+{
+   int block;
+
+   block = TO_NBLOCKS(format)/2 - 1;
+
+   while (block < TO_NBLOCKS(format)) {
+      if (fat_data[block] == TO_TAG_FREE)
+         return block;
+
+      if (fat_data[TO_NBLOCKS(format) - block - 2] == TO_TAG_FREE)  /* fat_data[-1] is always 0 */
+         return TO_NBLOCKS(format) - block - 2;
+
+      block++;
+   }
+
+   return -1;
+}
+
+
+
+/* find_free_block_rel:
+ *  Finds a free block using a relative search.
+ */
+static int find_free_block_rel(int format, int block, unsigned char fat_data[])
+{
+   if (block < TO_NBLOCKS(format)/2) {
+      while (block >= 0) {
+         if (fat_data[block] == TO_TAG_FREE)
+            break;
+
+         block--;
+      }
+   }
+   else {
+      while (block < TO_NBLOCKS(format)) {
+         if (fat_data[block] == TO_TAG_FREE)
+            break;
+
+         block++;
+      }
+   }
+
+   if ((block<0) || (block>=TO_NBLOCKS(format)))
+      block = find_free_block_sym(format, fat_data);
+
+   return block;
+}
+
+
+
+/* do_add_file:
+ *  Performs the low-level add operation for the specified file entry.
+ */
+static void do_add_file(sapID id, FILE *file, int file_size, unsigned char entry_data[], int n, unsigned char trk20_data[])
+{
+   sapsector_t sapsector;
+   unsigned char *fat_data;
+   unsigned int block, next_block;
+   int sect;
+   int dummy;
+
+   /* find the first block */
+   fat_data = trk20_data + TO_FAT_START(ID_FORMAT(id));
+
+   block = find_free_block_sym(ID_FORMAT(id), fat_data);
+   entry_data[TO_FIRST_BLOCK] = block;
+
+   sapsector.format = 0;
+   sapsector.protection = 0;
+
+   /* write full blocks */
+   while (file_size > TO_BLOCKSIZE(ID_FORMAT(id))) {
+      sapsector.track =  block/2;
+      sapsector.sector = 1 + (block%2 ? TO_SECTOR_PER_BLOCK : 0);
+      seek_pos(id, sapsector.track, sapsector.sector);
+
+      for (sect=0; sect<TO_SECTOR_PER_BLOCK; sect++) {
+         dummy=fread(sapsector.data, sizeof(char), TO_SECTSIZE(ID_FORMAT(id)), file);
+         do_write_sector(id, &sapsector);
+         sapsector.sector++;
+      }
+
+      fat_data[block] = TO_TAG_RESERVED;  /* temporarily reserved */
+      next_block = find_free_block_rel(ID_FORMAT(id), block, fat_data);
+
+      fat_data[block] = next_block;
+      block = next_block;
+
+      file_size -= TO_BLOCKSIZE(ID_FORMAT(id));
+   }
+
+   /* write remaining full sectors */
+   sapsector.track = block/2;
+   sapsector.sector = 1 + (block%2 ? TO_SECTOR_PER_BLOCK : 0);
+   seek_pos(id, sapsector.track, sapsector.sector);
+
+   fat_data[block] = TO_END_BLOCK_OFFSET + 1;
+
+   while (file_size > TO_SECTSIZE(ID_FORMAT(id))) {
+      dummy=fread(sapsector.data, sizeof(char), TO_SECTSIZE(ID_FORMAT(id)), file);
+      do_write_sector(id, &sapsector);
+      sapsector.sector++;
+
+      fat_data[block]++;
+      file_size -= TO_SECTSIZE(ID_FORMAT(id));
+   }
+
+   /* write remaining individual bytes */
+   dummy=fread(sapsector.data, sizeof(char), TO_SECTSIZE(ID_FORMAT(id)), file);
+   do_write_sector(id, &sapsector);
+
+   entry_data[TO_END_SIZE] = file_size>>8;
+   entry_data[TO_END_SIZE+1] = file_size;
+
+   /* write the new entry into the directory */
+   memcpy(trk20_data + TO_DIR_START(ID_FORMAT(id)) + n*TO_DIRENTRY_LENGTH, entry_data, TO_DIRENTRY_LENGTH);
+}
+
+
+
+/* do_delete_file:
+ *  Performs the low-level delete operation for the specified file entry and
+ *  returns the size of file.
+ */
+static int do_delete_file(sapID id, const char filename[], int n, unsigned char trk20_data[])
+{
+   unsigned char *fat_data, *entry_data;
+   int block, old_block, size = 0;
+
+   (void) filename;
+
+   fat_data = trk20_data + TO_FAT_START(ID_FORMAT(id));
+   entry_data = trk20_data + TO_DIR_START(ID_FORMAT(id)) + n*TO_DIRENTRY_LENGTH;
+
+   /* delete the file entry */
+   entry_data[TO_NAME] = 0;
+
+   /* read start block */
+   block = entry_data[TO_FIRST_BLOCK];
+
+   /* mark FAT entries with the TO_TAG_FREE tag */
+   while ((block<TO_NBLOCKS(ID_FORMAT(id))) && (size<TO_FILESIZE_MAX(ID_FORMAT(id)))) {
+      old_block = block;
+      block = fat_data[block];
+      fat_data[old_block] = TO_TAG_FREE;
+      size++;
+   }
+
+   size *= TO_BLOCKSIZE(ID_FORMAT(id));
+
+   if (block > TO_END_BLOCK_OFFSET+1)
+      size += (block - TO_END_BLOCK_OFFSET - 1) * TO_SECTSIZE(ID_FORMAT(id));
+
+   size += (entry_data[TO_END_SIZE]<<8) + entry_data[TO_END_SIZE+1];
+
+   return size;
+}
+
+
+
+/* do_extract_file:
+ *  Performs the low-level extract operation for the specified file entry and
+ *  returns the size of file.
+ */
+static int do_extract_file(sapID id, const char filename[], int n, unsigned char trk20_data[])
+{
+   sapsector_t sapsector;
+   unsigned char *fat_data, *entry_data;
+   int sect, end_size, size = 0;
+   unsigned int block;
+   FILE *file;
+
+   fat_data = trk20_data + TO_FAT_START(ID_FORMAT(id));
+   entry_data = trk20_data + TO_DIR_START(ID_FORMAT(id)) + n*TO_DIRENTRY_LENGTH;
+
+   /* read start block */
+   block = entry_data[TO_FIRST_BLOCK];
+
+   if ((file=fopen(filename, "wb")) == NULL) {
+      sap_errno = SAP_EPERM;
+      return 0;
+   }
+
+   /* extract full blocks */
+   while ((fat_data[block]<TO_NBLOCKS(ID_FORMAT(id))) && (size<TO_FILESIZE_MAX(ID_FORMAT(id)))) {
+      seek_pos(id, block/2, 1 + (block%2 ? TO_SECTOR_PER_BLOCK : 0));
+
+      for (sect=0; sect<TO_SECTOR_PER_BLOCK; sect++) {
+         do_read_sector(id, &sapsector);
+         fwrite(sapsector.data, sizeof(char), TO_SECTSIZE(ID_FORMAT(id)), file);
+      }
+
+      block = fat_data[block];
+      size++;
+   }
+
+   size *= TO_BLOCKSIZE(ID_FORMAT(id));
+
+   /* extract remaining full sectors */
+   seek_pos(id, block/2, 1 + (block%2 ? TO_SECTOR_PER_BLOCK : 0));
+
+   for (sect=0; sect<(fat_data[block] - TO_END_BLOCK_OFFSET - 1); sect++) {
+      do_read_sector(id, &sapsector);
+      fwrite(sapsector.data, sizeof(char), TO_SECTSIZE(ID_FORMAT(id)), file);
+      size += TO_SECTSIZE(ID_FORMAT(id));
+   }
+
+   /* extract remaining individual bytes */
+   do_read_sector(id, &sapsector);
+   end_size = (entry_data[TO_END_SIZE]<<8) + entry_data[TO_END_SIZE+1];
+   fwrite(sapsector.data, sizeof(char), end_size, file);
+   size += end_size;
+
+   fclose(file);
+
+   return size;
+}
+
+
+
+/************************************************/
+/***       public low-level functions         ***/
+/************************************************/
+
+
+/* _ExtractDir:
+ *  Extract the directory from track 20 and returns the number of lines.
+ */
+int _ExtractDir(char buffer[], int buffer_size, int drive, int density, const unsigned char trk20_data[])
+{
+   unsigned char disk_name[TO_NAME_LENGTH+1];
+   int dskf, i, len, pos, lines = 0;
+   int format = (density == 1 ? SAP_FORMAT2 : SAP_FORMAT1);
+
+   /* name of the volume */
+   strncpy((char*)disk_name, (char*)trk20_data, TO_NAME_LENGTH);
+
+   if ((disk_name[0] == 0) || (disk_name[0] == TO_TAG_FREE)) {
+      strcpy((char*)disk_name, "No Name ");
+   }
+   else {
+      disk_name[TO_NAME_LENGTH] = 0;
+      clean_string(disk_name);
+   }
+
+   /* dskf */
+   dskf = get_dskf(format, trk20_data) * ((TO_BLOCKSIZE(format) + 1024) / 1024);
+
+   /* header */
+   pos = sprintf(buffer, "%s density   %d:%s DSKF = %d\n", (density == 1 ? "Single" : "Double"), drive, disk_name, dskf);
+   lines++;
+
+   /* directory entries */
+   for (i=0; i<TO_NDIRENTRIES(format); i++) {
+      len = extract_dir_entry(format, buffer+pos, buffer_size-pos, i, trk20_data);
+
+      if (len>0) {
+         pos += len;
+         lines++;
+      }
+   }
+
+   return lines;
+}
+
+
+
+/* _ForEachFile:
+ *  Executes the specified callback funtion on each file matching the pattern.
+ */
+int _ForEachFile(sapID id, const char pattern[], sapfilecb_t callback, int save_back)
+{
+   char filename[TO_NAME_LENGTH + 1 + TO_EXT_LENGTH + 1];
+   unsigned char trk20_data[SAP_TRACKSIZE1], *dir_data;
+   int n, ret = 0;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return 0;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return 0;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return 0;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   /* read track 20 */
+   sap_ReadSectorEx(id, 20, 1, SAP_NSECTS, trk20_data);
+
+   dir_data = trk20_data + TO_DIR_START(ID_FORMAT(id));
+
+   if (strpbrk(pattern, "?*")) {  /* wildcards? */
+      while ((n=seek_wildcard(ID_FORMAT(id), filename, pattern, dir_data)) >= 0)
+         ret += callback(id, filename, n, trk20_data);
+   }
+   else {
+      n = seek_file(ID_FORMAT(id), pattern, dir_data);
+
+      if (n<0)
+         sap_errno = SAP_ENOENT;
+      else
+         ret = callback(id, pattern, n, trk20_data);
+   }
+
+   if (save_back) {
+      /* save track 20 */
+      sap_WriteSectorEx(id, 20, 1, SAP_NSECTS, trk20_data);
+   }
+
+   return ret;
+}
+
+
+
+/************************************************/
+/***  API functions: physical format support  ***/
+/************************************************/
+
+
+/* OpenArchive:
+ *  Opens an existing archive,
+ *  returns the sapID on success or SAP_ERROR on error.
+ */
+sapID sap_OpenArchive(const char filename[], int *format)
+{
+   char header[SAP_HEADER_SIZE];
+   sapID id;
+   int dummy;
+
+   /* seek for an empty id */
+   if (!(id=get_id())) {
+      sap_errno = SAP_ETOOMANY;
+      return SAP_ERROR;
+   }
+
+   if ((ID_FILE(id)=fopen(filename, "rb+")) == NULL) {
+      sap_errno = SAP_ENOENT;
+      return SAP_ERROR;
+   }
+
+   /* read the header */ 
+   dummy=fread(header, sizeof(char), SAP_HEADER_SIZE, ID_FILE(id));
+
+   /* find the format */
+   if ((header[0] != SAP_FORMAT1) && (header[0] != SAP_FORMAT2)) {
+      fclose(ID_FILE(id));
+      sap_errno = SAP_EBADF;
+      return SAP_ERROR;
+   }
+
+   *format = header[0];
+   
+   /* check the header */
+   header[0] = 0;
+
+   if (strncmp(header, sap_header, SAP_HEADER_SIZE) != 0) {
+      fclose(ID_FILE(id));
+      sap_errno = SAP_EBADF;
+      return SAP_ERROR;
+   }
+
+   ID_FORMAT(id) = *format;
+   ID_STATE(id) = FULL_ARCHIVE;
+   ID_NTRACKS(id) = SAP_NTRACKS(*format);
+
+   return id;
+}
+
+
+
+/* CreateArchive:
+ *  Creates an archive skeleton and writes the header,
+ *  returns the sapID on success or SAP_ERROR on error.
+ */
+sapID sap_CreateArchive(const char filename[], int format)
+{
+   char header[SAP_HEADER_SIZE];
+   sapID id;
+
+   if ((format != SAP_FORMAT1) && (format != SAP_FORMAT2)) {
+      sap_errno = SAP_EINVAL;
+      return SAP_ERROR;
+   }
+
+   /* seek for an empty id */
+   if (!(id=get_id())) {
+      sap_errno = SAP_ETOOMANY;
+      return SAP_ERROR;
+   }
+
+   if ((ID_FILE(id)=fopen(filename, "wb")) == NULL) {
+      sap_errno = SAP_EPERM;
+      return SAP_ERROR;
+   }
+
+   /* write the header */
+   memcpy(header, sap_header, SAP_HEADER_SIZE);
+   header[0] = format;
+
+   fwrite(header, sizeof(char), SAP_HEADER_SIZE, ID_FILE(id));
+
+   ID_FILE(id) = freopen(filename, "rb+", ID_FILE(id));
+   seek_pos(id, 0 ,1);
+
+   ID_FORMAT(id) = format;
+   ID_STATE(id) = EMPTY_ARCHIVE;
+   ID_NTRACKS(id) = 0;
+
+   return id;
+}
+
+
+
+/* CloseArchive:
+ *  Closes the archive, filling it up with empty tracks if needed,
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_CloseArchive(sapID id)
+{
+   sapsector_t sapsector;
+   int format = ID_FORMAT(id);
+   int track, sect;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+      case FILLED_ARCHIVE:
+         sapsector.format = 0;
+         sapsector.protection = 0;
+         memset(sapsector.data, TO_FILLER_BYTE, SAP_SECTSIZE(format));
+
+         for (track=ID_NTRACKS(id); track<SAP_NTRACKS(format); track++) {
+            for (sect=1; sect<=SAP_NSECTS; sect++) {
+               sapsector.track = track;
+               sapsector.sector = sect;
+               do_write_sector(id, &sapsector);
+            }
+         }
+         break;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   fclose(ID_FILE(id));
+   ID_STATE(id) = NO_ARCHIVE;
+
+   return SAP_OK;
+}
+
+
+
+/* FillArchive:
+ *  Fills an empty archive sector by sector, it's up to
+ *  the user to call it the right number of times,
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_FillArchive(sapID id, sapsector_t *sapsector)
+{
+   static int sect;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+         ID_STATE(id) = FILLED_ARCHIVE;
+         sect = 1;
+         /* no break */
+
+      case FILLED_ARCHIVE:
+         do_write_sector(id, sapsector);
+
+         if (++sect == SAP_NSECTS+1) {
+            sect = 1;
+
+            if (++ID_NTRACKS(id) == SAP_NTRACKS(ID_FORMAT(id)))
+               ID_STATE(id) = FULL_ARCHIVE;
+         }
+         break;
+
+      case FULL_ARCHIVE:
+         sap_errno = SAP_ENOSPC;
+         return SAP_ERROR;
+   } /* end of switch */
+
+   return SAP_OK;
+}
+
+
+
+/* ReadSector:
+ *  Reads the specified sector and returns SAP_OK or a flagged code:
+ *    SAP_NO_STD_FMT: non standard format
+ *    SAP_PROTECTED : protected sector
+ *    SAP_BAD_SECTOR: bad sector identifiers
+ *    SAP_CRC_ERROR : CRC error
+ */
+int sap_ReadSector(sapID id, int track, int sect, sapsector_t *sapsector)
+{
+   int flag = SAP_OK;
+   int format = ID_FORMAT(id);
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EEMPTY;
+         return SAP_ERROR;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return SAP_ERROR;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   seek_pos(id, track, sect);
+   do_read_sector(id, sapsector);
+
+   if (sapsector->format != 0)
+      flag |= SAP_NO_STD_FMT;
+
+   if (sapsector->protection != 0)
+      flag |= SAP_PROTECTED;
+
+   if ((sapsector->track != track) || (sapsector->sector != sect))
+      flag |= SAP_BAD_SECTOR;
+
+   do_crc(sapsector, format);
+
+   if ((sapsector->data[SAP_SECTSIZE(format)] != ((crcpuk_temp>>8)&0xff))
+        || (sapsector->data[SAP_SECTSIZE(format)] != (crcpuk_temp&0xff)))
+      flag |= SAP_CRC_ERROR;
+
+   return flag;
+}
+
+
+
+/* ReadSectorEx:
+ *  Reads one or more sectors from the same track,
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_ReadSectorEx(sapID id, int track, int sect, int nsects, unsigned char data[])
+{
+   sapsector_t sapsector;
+   int format = ID_FORMAT(id);
+   int i;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EEMPTY;
+         return SAP_ERROR;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return SAP_ERROR;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   seek_pos(id, track, sect);
+
+   for (i=0; i<nsects; i++) {
+      do_read_sector(id, &sapsector);
+      memcpy(data + i*SAP_SECTSIZE(format), sapsector.data, SAP_SECTSIZE(format));
+   }
+
+   return SAP_OK;
+}
+
+
+
+/* WriteSector:
+ *  Writes the specified sector and
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_WriteSector(sapID id, int track, int sect, sapsector_t *sapsector)
+{
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EEMPTY;
+         return SAP_ERROR;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return SAP_ERROR;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   seek_pos(id, track, sect);
+   do_write_sector(id, sapsector);
+
+   return SAP_OK;
+}
+
+
+
+/* WriteSectorEx:
+ *  Writes one or more sectors into the same track,
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_WriteSectorEx(sapID id, int track, int sect, int nsects, const unsigned char data[])
+{
+   sapsector_t sapsector;
+   int format = ID_FORMAT(id);
+   int i;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EEMPTY;
+         return SAP_ERROR;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return SAP_ERROR;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   seek_pos(id, track, sect);
+
+   sapsector.format = 0;
+   sapsector.protection = 0;
+   sapsector.track = track;
+
+   for (i=0; i<nsects; i++) {
+      memcpy(sapsector.data, data + i*SAP_SECTSIZE(format), SAP_SECTSIZE(format));
+      sapsector.sector = sect + i;
+      do_write_sector(id, &sapsector);
+   }
+
+   return SAP_OK;
+}
+
+
+
+/************************************************/
+/***  API functions: logical format support   ***/
+/************************************************/
+
+
+/* FormatArchive:
+ *  Formats an archive using the Thomson BASIC DOS format,
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_FormatArchive(sapID id, int capacity)
+{
+   int format = ID_FORMAT(id);
+   int track, sect;
+
+   sapsector_t sapsector;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return SAP_ERROR;
+
+      case EMPTY_ARCHIVE:
+         break;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return SAP_ERROR;
+
+      case FULL_ARCHIVE:
+         seek_pos(id, 0, 1);
+         break;
+   }
+
+   if (!(capacity == SAP_TRK40) && !(capacity == SAP_TRK80)) {
+      sap_errno = SAP_EINVAL;
+      return SAP_ERROR;
+   }
+
+   sapsector.format = 0;
+   sapsector.protection = 0;
+   memset(sapsector.data, TO_FILLER_BYTE, SAP_SECTSIZE(format));
+
+   for (track=0; track<SAP_NTRACKS(format); track++) {
+      for (sect=1; sect<SAP_NSECTS+1; sect++) {
+         sapsector.track = track;
+         sapsector.sector = sect;
+         do_write_sector(id, &sapsector);
+      }
+   }
+
+   /* write track 20 */
+   sapsector.track = 20;
+   memset(sapsector.data, TO_TAG_FREE, SAP_SECTSIZE(format));
+   seek_pos(id, 20, 1);
+
+   for (sect=1; sect<SAP_NSECTS+1; sect++) {
+      sapsector.sector = sect;
+      do_write_sector(id, &sapsector);
+   }
+
+   /* FAT */
+   sapsector.track = 20;
+   sapsector.sector = 2;
+   memset(sapsector.data, TO_TAG_RESERVED, SAP_SECTSIZE(format));
+
+   /* first byte */
+   sapsector.data[0] = 0;
+
+   /* lower zone */
+   memset(sapsector.data + 1, TO_TAG_FREE, 40);
+
+   /* upper zone */
+   if (capacity == SAP_TRK80)  /* not (format == SAP_FORMAT1) */
+      memset(sapsector.data + 43, TO_TAG_FREE, TO_NBLOCKS1 - 43 + 1);
+   else
+      memset(sapsector.data + 43, TO_TAG_FREE, TO_NBLOCKS2 - 43 + 1);
+
+   seek_pos(id, 20, 2);
+   do_write_sector(id, &sapsector);
+
+   ID_STATE(id) = FULL_ARCHIVE;
+   ID_NTRACKS(id) = SAP_NTRACKS(format);
+
+   return SAP_OK;
+}
+
+
+
+/* ListArchive:
+ *  Builds a list of files contained in the archive,
+ *  returns the number of lines on success or 0 on error.
+ */
+int sap_ListArchive(sapID id, char buffer[], int buffer_size)
+{
+   unsigned char trk20_data[SAP_TRACKSIZE1];
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return 0;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EEMPTY;
+         return 0;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return 0;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   /* read track 20 */
+   sap_ReadSectorEx(id, 20, 1, SAP_NSECTS, trk20_data);
+
+   return _ExtractDir(buffer, buffer_size, 0, ID_FORMAT(id) == SAP_FORMAT1 ? 2 : 1, trk20_data);
+}
+
+
+
+/* AddFile:
+ *  Adds the specified file to the archive,
+ *  returns the size of the file in bytes on success or 0 on error.
+ */
+int sap_AddFile(sapID id, const char filename[])
+{
+   unsigned char entry_data[TO_DIRENTRY_LENGTH];
+   unsigned char trk20_data[SAP_TRACKSIZE1], *fat_data, *dir_data;
+   int free_n=-1, prev_n=-1;
+   int i, dskf, file_size = 0;
+   FILE *file;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return 0;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return 0;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return 0;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   /* open the file */
+   if ((file=fopen(filename, "rb")) == NULL) {
+      sap_errno = SAP_ENOENT;
+      return 0;
+   }
+
+   /* find size of the file */
+   while (fgetc(file) != EOF)
+      file_size++;
+
+   if (file_size == 0) {
+      fclose(file);
+      sap_errno = SAP_ENFILE;
+      return 0;
+   }
+
+   fseek(file, 0, SEEK_SET);
+
+   /* decode the filename */
+   decode_filename(entry_data, filename, file_size);
+
+   /* read track 20 */
+   sap_ReadSectorEx(id, 20, 1, SAP_NSECTS, trk20_data);
+
+   fat_data = trk20_data + TO_FAT_START(ID_FORMAT(id));
+   dir_data = trk20_data + TO_DIR_START(ID_FORMAT(id));
+
+   /* simultaneously seek already free entry and previous entry */
+   for (i=0; i<TO_NDIRENTRIES(ID_FORMAT(id)); i++) {
+      if ((dir_data[i*TO_DIRENTRY_LENGTH] == 0) || (dir_data[i*TO_DIRENTRY_LENGTH] == TO_TAG_FREE)) {
+         if (free_n<0)
+            free_n = i;
+      }
+      else {
+         if (strncmp((char*)(dir_data+i*TO_DIRENTRY_LENGTH),(char*) entry_data, TO_NAME_LENGTH + TO_EXT_LENGTH) == 0) {
+            prev_n = i;
+            break;
+         }
+      }
+   }
+
+   if ((free_n<0) && (prev_n<0)) {
+      fclose(file);
+      sap_errno = SAP_ENOSPC;
+      return 0;
+   }
+
+   /* test for enough free disk space */
+   dskf = get_dskf(ID_FORMAT(id), trk20_data);
+
+   if (prev_n >=0)
+      dskf += get_file_size(ID_FORMAT(id), prev_n, trk20_data);
+
+   if ((dskf*TO_BLOCKSIZE(ID_FORMAT(id))) < file_size) {
+      sap_errno = SAP_EFBIG;
+      fclose(file);
+      return 0;
+   }
+
+   /* delete previous entry */
+   if (prev_n >= 0) {
+      do_delete_file(id, filename, prev_n, trk20_data);
+      free_n = prev_n;
+   }
+
+   /* phew! we can finally add the file... */
+   do_add_file(id, file, file_size, entry_data, free_n, trk20_data);
+
+   /* update directory and FAT */
+   sap_WriteSectorEx(id, 20, 1, SAP_NSECTS, trk20_data);
+
+   fclose(file);
+
+   return file_size;
+}
+
+
+
+/* DeleteFile:
+ *  Deletes the specified files from the archive,
+ *  returns the total size of the files in bytes on success or 0 on error.
+ */
+int sap_DeleteFile(sapID id, const char pattern[])
+{
+   return _ForEachFile(id, pattern, do_delete_file, 1);
+}
+
+
+
+/* ExtractFile:
+ *  Extracts the specified files from the archive,
+ *  returns the total size of the files in bytes on success or 0 on error.
+ */
+int sap_ExtractFile(sapID id, const char pattern[])
+{
+   return _ForEachFile(id, pattern, do_extract_file, 0);
+}
+
+
+
+/* GetFileInfo:
+ *  Fills in a structure with the info for the specified file
+ *  returns SAP_OK on success or SAP_ERROR on error.
+ */
+int sap_GetFileInfo(sapID id, const char filename[], sapfileinfo_t *info)
+{
+   unsigned char trk20_data[SAP_TRACKSIZE1], *dir_data, *fat_data, *entry_data;
+   struct tm tim;
+   int n, block, i=0;
+
+   switch (ID_STATE(id)) {
+
+      case NO_ARCHIVE:
+         sap_errno = SAP_EINVAL;
+         return 0;
+
+      case EMPTY_ARCHIVE:
+         sap_errno = SAP_EEMPTY;
+         return 0;
+
+      case FILLED_ARCHIVE:
+         sap_errno = SAP_EBUSY;
+         return 0;
+
+      case FULL_ARCHIVE:
+         break;
+   }
+
+   /* read track 20 */
+   sap_ReadSectorEx(id, 20, 1, SAP_NSECTS, trk20_data);
+
+   dir_data = trk20_data + TO_DIR_START(ID_FORMAT(id));
+
+   n = seek_file(ID_FORMAT(id), filename, dir_data);
+
+   if (n<0) {
+      sap_errno = SAP_ENOENT;
+      return SAP_ERROR;
+   }
+
+   fat_data = trk20_data + TO_FAT_START(ID_FORMAT(id));
+   entry_data = trk20_data + TO_DIR_START(ID_FORMAT(id)) + n*TO_DIRENTRY_LENGTH;
+
+   /* block chain */
+   info->nblocks = get_file_size(ID_FORMAT(id), n, trk20_data);
+   info->block = malloc(info->nblocks*sizeof(int));
+
+   block = entry_data[TO_FIRST_BLOCK];
+
+   while ((block<TO_NBLOCKS(ID_FORMAT(id))) && (i<TO_FILESIZE_MAX(ID_FORMAT(id)))) {
+      info->block[i++] = block;
+      block = fat_data[block];
+   }
+
+   /* common attributes */
+   info->size = info->nblocks*TO_BLOCKSIZE(ID_FORMAT(id)) + entry_data[TO_END_SIZE];
+   info->file_type = entry_data[TO_FILE_TYPE];
+   info->data_type = entry_data[TO_DATA_TYPE];
+
+   /* date */
+   if ((entry_data[TO_DATE_DAY] >= 1) && (entry_data[TO_DATE_DAY] <= 31) &&
+       (entry_data[TO_DATE_MONTH] >= 1) && (entry_data[TO_DATE_MONTH] <= 12) &&
+       (entry_data[TO_DATE_YEAR] <= 99)) {
+      memset(&tim, 0, sizeof(struct tm));
+      tim.tm_mday = entry_data[TO_DATE_DAY];
+      tim.tm_mon = entry_data[TO_DATE_MONTH];
+      tim.tm_year = entry_data[TO_DATE_YEAR];
+      info->date = mktime(&tim);
+   }
+   else {
+      info->date = 0;
+   }
+
+   /* comment */
+   info->comment[0] = '\0';
+   if (entry_data[TO_COMMENT])
+      strncat(info->comment,(char*)( entry_data+TO_COMMENT), TO_COMMENT_LENGTH);
+
+   return SAP_OK;
+}
+
Index: tools/sap/libsap.h
===================================================================
--- tools/sap/libsap.h	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/libsap.h	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,119 @@
+/*  LibSAP
+ *  Version 0.9.3
+ *  Copyright (C) 2000-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#ifndef LIBSAP_H
+#define LIBSAP_H
+
+#include <stdio.h>
+#include <time.h>
+
+
+/* constants for the SAP physical file formats */
+#define SAP_NSECTS      16
+
+#define SAP_NTRACKS1    80
+#define SAP_SECTSIZE1   256
+#define SAP_TRACKSIZE1  (SAP_NSECTS*SAP_SECTSIZE1)
+
+#define SAP_NTRACKS2    40
+#define SAP_SECTSIZE2   128
+#define SAP_TRACKSIZE2  (SAP_NSECTS*SAP_SECTSIZE2)
+
+
+/* types used by the library */
+typedef unsigned int sapID;
+
+typedef struct {
+   unsigned char format;
+   unsigned char protection;
+   unsigned char track;
+   unsigned char sector;
+   unsigned char data[258];
+} sapsector_t;  /* type of a SAP sector */
+
+typedef struct {
+   int size;
+   int file_type;
+   int data_type;
+   time_t date;
+   char comment[9];
+   int nblocks;
+   int *block;
+} sapfileinfo_t;
+
+typedef int (*sapfilecb_t)(sapID id, const char filename[], int n, unsigned char trk20_data[]);
+
+
+/* return values */
+#define SAP_OK          0
+#define SAP_NO_STD_FMT  (1<<0)
+#define SAP_PROTECTED   (1<<1)
+#define SAP_BAD_SECTOR  (1<<2)
+#define SAP_CRC_ERROR   (1<<3)
+#define SAP_ERROR       (1<<7)
+
+
+/* errno values */
+extern int sap_errno;
+
+#define SAP_EBADF     1
+#define SAP_EBUSY     2
+#define SAP_EEMPTY    3
+#define SAP_EFBIG     4
+#define SAP_EINVAL    5
+#define SAP_ENFILE    6
+#define SAP_ENOENT    7
+#define SAP_ENOSPC    8
+#define SAP_EPERM     9
+#define SAP_ETOOMANY  10
+
+
+/* low-level functions */
+extern int _ExtractDir(char buffer[], int buffer_size, int drive, int density, const unsigned char trk20_data[]);
+extern int _ForEachFile(sapID id, const char pattern[], sapfilecb_t callback, int save_back);
+
+
+/* physical format API functions */
+#define SAP_FORMAT1  1
+#define SAP_FORMAT2  2
+
+extern sapID sap_OpenArchive(const char filename[], int *format);
+extern sapID sap_CreateArchive(const char filename[], int format);
+extern int   sap_CloseArchive(sapID id);
+extern int   sap_FillArchive(sapID id, sapsector_t *sector);
+extern int   sap_ReadSector(sapID id, int track, int sect, sapsector_t *sector);
+extern int   sap_ReadSectorEx(sapID id, int track, int sect, int nsects, unsigned char data[]);
+extern int   sap_WriteSector(sapID id, int track, int sect, sapsector_t *sector);
+extern int   sap_WriteSectorEx(sapID id, int track, int sect, int nsects, const unsigned char data[]);
+
+
+/* logical format API functions */
+#define SAP_TRK40   1
+#define SAP_TRK80   2
+
+extern int   sap_FormatArchive(sapID id, int capacity);
+extern int   sap_ListArchive(sapID id, char buffer[], int buffer_size);
+extern int   sap_AddFile(sapID id, const char filename[]);
+extern int   sap_DeleteFile(sapID id, const char pattern[]);
+extern int   sap_ExtractFile(sapID id, const char pattern[]);
+extern int   sap_GetFileInfo(sapID id, const char filename[], sapfileinfo_t *info);
+
+#endif
+
Index: tools/sap/libsap.txt
===================================================================
--- tools/sap/libsap.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/libsap.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,191 @@
+LibSAP est une librarie de manipulation des archives SAP écrite en ANSI C.
+Elle fournit à la fois des routines de manipulation bas-niveau des archives
+SAP considérées comme format physique (images de disquettes) et des routines
+de manipulation haut-niveau des archives SAP considérées comme format
+logique (compatible avec le DOS BASIC Thomson).
+
+
+
+1. Routines de manipulation du format physique:
+
+sapID sap_OpenArchive(const char filename[], int *format);
+   Cette routine ouvre l'archive SAP dont le nom est spécifié par 'filename' et
+  retourne le numéro d'identification (sapID) attaché à cette archive à partir
+  de cet instant. Ce numéro d'identification sera passé aux autres routines de
+  la librairie pour désigner l'archive SAP sur laquelle elle devront agir.
+   Le paramètre 'format' retourne le format de l'archive SAP, qui peut-être
+  SAP_FORMAT1 ou SAP_FORMAT2.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_ETOOMANY: trop d'archives SAP sont ouvertes simultanément.
+  SAP_ENOENT: l'archive SAP spécifiée n'existe pas.
+  SAP_EBADF: le fichier spécifié n'est pas une archive SAP.
+
+
+sapID sap_CreateArchive(const char filename[], int format);
+   Cette routine crée une archive SAP dont le nom est spécifié par 'filename'
+  et retourne le numéro d'identification (sapID) attaché à cette archive à
+  partir de cet instant. Ce numéro d'identification sera passé aux autres
+  routines de la librairie pour désigner l'archive SAP sur laquelle elle
+  devront agir.
+   Le paramètre 'format' spécifie le format de l'archive SAP, qui peut-être
+  SAP_FORMAT1 ou SAP_FORMAT2.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_ETOOMANY: trop d'archives SAP sont ouvertes simultanément.
+  SAP_EPERM: impossible de créer le fichier sur le support d'enregistrement.
+
+
+int sap_CloseArchive(sapID id);
+   Cette routine ferme une archive SAP ouverte par sap_OpenArchive() ou créée
+  par sap_CreateArchive() et retourne SAP_OK.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+
+
+int sap_FillArchive(sapID id, sapsector_t *sapsector);
+   Cette routine remplie une archive créée par sap_CreateArchive() secteur
+  par secteur, en commençant par le secteur 1 de la piste 0. A chaque appel
+  le numéro de secteur est incrémenté de 1 et, si la piste courante devient
+  pleine, la piste suivante est sélectionnée. Retourne SAP_OK.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_ENOSPC: l'archive SAP est pleine.
+
+
+int sap_ReadSector(sapID id, int track, int sect, sapsector_t *sapsector);
+   Cette routine lit le secteur numéro 'sect' de la piste numéro 'track' et
+  place le résultat dans la structure désignée par 'sapsector', retournant
+  SAP_OK ou une combinaison des flags suivants:
+  SAP_NO_STD_FMT: le format du secteur est non standard.
+  SAP_PROTECTED : le secteur est protégé en écriture.
+  SAP_BAD_SECTOR: le secteur a de mauvais identificateurs (piste, secteur)
+  SAP_CRC_ERROR : erreur de CRC sur les données du secteur.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+
+
+int sap_ReadSectorEx(sapID id, int track, int sect, int nsects, unsigned char data[]);
+   Cette routine lit 'nsects' secteurs de la piste 'track' à partir du
+  secteur 'sect' et place le résultat dans le tableau 'data'. Retourne
+  SAP_OK.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+
+
+int sap_WriteSector(sapID id, int track, int sect, sapsector_t *sapsector);
+   Cette routine écrit le secteur désigné par 'sapsector' au secteur numéro
+  'sect' de la piste numéro 'track'. Tous les champs du secteur doivent être
+  spécifiés, exceptés les deux champs relatifs au CRC qui seront calculés
+  par la routine elle-même. Retourne SAP_OK.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+
+
+int sap_WriteSectorEx(sapID id, int track, int sect, int nsects, const unsigned char data[])
+   Cette routine écrit 'nsects' secteurs dans la piste 'track' à partir du
+  secteur 'sect' en utilisant les données du tableau 'data'. Retourne
+  SAP_OK.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+
+
+
+2. Routines de manipulation du format logique:
+
+int sap_FormatArchive(sapID id, int capacity);
+   Cette routine formate une archive SAP au format DOS BASIC Thomson et
+  retourne SAP_OK. Elle supporte les capacités SAP_TRK80 (80 pistes) et
+  SAP_TRK40 (40 pistes). Elle peut être utilisée à la fois sur une archive
+  créée par sap_CreateArchive() ou ouverte par sap_OpenArchive().
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) ou le format est invalide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+
+
+int sap_ListArchive(sapID id, char buffer[], int buffer_size);
+   Cette routine copie la liste des fichiers (au sens du DOS BASIC Thomson)
+  présents dans l'archive SAP dans le tampon 'buffer' dont la taille est en
+  octets est spécifiée par 'buffer_size' et retourne le nombre de ligne de
+  cette liste.
+   Si une erreur survient, la routine retourne 0 et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+
+
+int sap_AddFile(sapID id, const char filename[]);
+   Cette routine ajoute le fichier spécifié par 'filename' à l'archive SAP
+  (en tant que fichier au sens du DOS BASIC Thomson) et retourne la taille
+  du fichier en octets.
+   Si une erreur survient, la routine retourne 0 et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+  SAP_ENOENT: le fichier n'existe pas.
+  SAP_ENFILE: le fichier est vide.
+  SAP_ENOSPC: le répertoire de l'archive SAP est plein.
+  SAP_EFBIG: le fichier est trop gros pour l'espace libre de l'archive SAP.
+
+
+int sap_DeleteFile(sapID id, const char pattern[]);
+   Cette routine efface le fichier spécifié par 'pattern' de l'archive SAP
+  et retourne la taille du fichier en octets. Elle traite la présence des
+  wildcards '*' et '?' dans la chaîne 'pattern' et retourne dans ce cas la
+  taille totale de tous les fichiers effacés (éventuellement nulle).
+   Si une erreur survient, la routine retourne 0 et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+  SAP_ENOENT: le fichier n'existe pas dans l'archive SAP (erreur non valide
+              lorsque 'pattern' contient au moins une wilcard).
+
+
+int sap_ExtractFile(sapID id, const char pattern[]);
+   Cette routine extrait le fichier spécifié par 'pattern' de l'archive SAP
+  et retourne la taille du fichier en octets. Elle traite la présence des
+  wildcards '*' et '?' dans la chaîne 'pattern' et retourne dans ce cas la
+  taille totale de tous les fichiers extraits (éventuellement nulle).
+   Si une erreur survient, la routine retourne 0 et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+  SAP_ENOENT: le fichier n'existe pas dans l'archive SAP (erreur non valide
+              lorsque 'pattern' contient au moins une wilcard).
+  SAP_EPERM: impossible d'écrire le ou les fichiers sur le support.
+
+
+
+int sap_GetFileInfo(sapID id, const char filename[], sapfileinfo_t *info);
+   Cette routine remplit les champs de la structure 'info' avec les
+  informations relatives au fichier 'filename' spécifié. Retourne SAP_OK.
+   Si une erreur survient, la routine retourne SAP_ERROR et stocke le numéro
+  d'erreur dans la variable sap_errno:
+  SAP_EINVAL: le numéro d'identification (sapID) est invalide.
+  SAP_EEMPTY: l'archive SAP est vide.
+  SAP_EBUSY: l'archive SAP est en train d'être remplie par sap_FillArchive()
+  SAP_ENOENT: le fichier n'existe pas dans l'archive SAP.
+
+
+
+Eric Botcazou
+e-mail: <ebotcazou@libertysurf.fr>
Index: tools/sap/licence.txt
===================================================================
--- tools/sap/licence.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/licence.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,340 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    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; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
Index: tools/sap/sap2.c
===================================================================
--- tools/sap/sap2.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/sap2.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,853 @@
+/*  SAP2
+ *  Version 2.1
+ *  Copyright (C) 2000-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+/*  version 1.0: programme fonctionnel sous MSDOS et Linux
+ *          1.1: retour au format PC à la sortie du programme
+ *          1.2: ajout d'un mode de fonctionnement batch
+ *          2.0: support des disquettes 5"25 et de deux lecteurs PC
+ *          2.1: support de la simple densité
+ */
+
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "libsap.h"
+#include "floppy.h"
+
+
+#define SAP2_VERSION_STR "2.0.93"
+
+#ifdef linux
+   #define SAP2_PLATFORM_STR "Linux"
+#else
+   #define SAP2_PLATFORM_STR "MSDOS"
+#endif
+
+#define FILENAME_LENGTH 128
+
+
+/* horrible hack pour supporter les accents... */
+#ifdef linux
+static char eacute[] = "é";
+static char egrave[] = "è";
+static char agrave[] = "à";
+static char ugrave[] = "ù";
+#else
+static char eacute[] = "";
+static char egrave[] = "";
+static char agrave[] = "
+";
+static char ugrave[] = "";
+#endif
+
+
+/* pour l'affichage du répertoire */
+#define PAGE_HEIGHT 22
+
+
+static struct floppy_info fi;
+static char *drive_type_name[7] = { "non disponible",
+                                    "5\"25 - 360 ko",
+                                    "5\"25 - 1.2 Mo",
+                                    "3\"5 -  720 ko",
+                                    "3\"5 - 1.44 Mo",
+                                    "3\"5 - 2.88 Mo",
+                                    "3\"5 - 2.88 Mo" };
+
+#define IS_5_INCHES(drive) ((fi.drive_type[drive]>0) && (fi.drive_type[drive]<3))
+
+
+
+/* term_puts:
+ *  Affiche une chaîne de caractères en respectant la taille du terminal.
+ */
+static void term_puts(const char buffer[], int lines, int page_height)
+{
+   const char *start_cur, *end_cur;
+   char trash[8];
+   int i;
+   char* ret;
+
+   start_cur = end_cur = buffer;
+
+   while (lines>page_height) {
+      for (i=0; i<page_height; i++) {
+         while (*end_cur++ != '\n')
+            ;
+      }
+
+      fwrite(start_cur, sizeof(char), end_cur-start_cur, stdout);
+
+      ret=fgets(trash, 7, stdin);
+      printf("Appuyer sur <Return>");
+      getchar();
+      printf("\n");
+      ungetc('\n', stdin);
+
+      start_cur = end_cur;
+      lines -= page_height;
+   }
+
+   puts(end_cur);
+}
+
+
+
+/* ViewDiskDir:
+ *  Affiche le répertoire d'une disquette TO.
+ */
+static int ViewDiskDir(int drive, int density, int page_height)
+{
+   unsigned char data[512*SAP_NSECTS];
+   char buffer[4096];
+   int lines;
+
+   if (FloppyReadSector(drive, density, 20, 1, SAP_NSECTS, data) != 0)
+      return 1;
+
+   /* on réduit la FAT à 80 blocks pour les disquettes 5"25 DD */
+   if (IS_5_INCHES(drive) && density == 2)
+      memset(data + SAP_SECTSIZE1 + 1 + SAP_NTRACKS1, 0xFE, SAP_NTRACKS1);
+
+   /* fonction bas-niveau de libSAP */
+   lines = _ExtractDir(buffer, sizeof(buffer), drive, density, data);
+
+   if ((page_height<0) || (lines<=page_height))
+      puts(buffer);
+   else
+      term_puts(buffer, lines, page_height);
+
+   return 0;
+}
+
+
+
+/* ViewArchiveDir:
+ *  Affiche le répertoire d'une archive SAP.
+ */
+static int ViewArchiveDir(const char sap_name[], int page_height)
+{
+   char buffer[4096];
+   sapID sap_file;
+   int format, lines;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR)
+      return 1;
+
+   lines = sap_ListArchive(sap_file, buffer, sizeof(buffer));
+
+   if (lines == 0)
+      return 2;
+
+   if ((page_height<0) || (lines<=page_height))
+      puts(buffer);
+   else
+      term_puts(buffer, lines, page_height);
+
+   sap_CloseArchive(sap_file);
+   return 0;
+}
+
+
+
+/* CreateEmptyArchive:
+ *  Crée une archive SAP vide (mais formatée).
+ */
+static int CreateEmptyArchive(const char sap_name[], int format, int capacity)
+{
+   sapID sap_file;
+
+   if ((sap_file=sap_CreateArchive(sap_name, format)) == SAP_ERROR)
+      return 1;
+
+   sap_FormatArchive(sap_file, capacity);
+   sap_CloseArchive(sap_file);
+
+   return 0;
+}
+
+
+
+/* BuildSectorMap:
+ *  Construit la carte des secteurs d'une piste
+ *  en fonction du facteur d'entrelacement.
+ */
+static void BuildSectorMap(int *sector_map, int factor)
+{
+   int sect, loc=0;
+
+   /* mise à zéro de la table */
+   memset(sector_map, 0, sizeof(int)*SAP_NSECTS);
+
+   for (sect=1; sect<=SAP_NSECTS; sect++) {
+      while (sector_map[loc] != 0)
+         loc=(loc+1)%SAP_NSECTS;
+
+      sector_map[loc]=sect;
+
+      loc=(loc+factor)%SAP_NSECTS;
+   }
+}
+
+
+
+/* FormatDisk:
+ *  Formate une disquette 3"5 - 720 ko au format Thomson.
+ */
+static int FormatDisk(int drive, int density, int factor, int verbose)
+{
+   int num_tracks, sect_size;
+   int track, sect, pos, i;
+   int sector_map[SAP_NSECTS];
+   unsigned char header_table[512];
+   unsigned char buffer[512*SAP_NSECTS];
+
+   /* détermination du nombre de pistes */
+   if (IS_5_INCHES(drive))
+      num_tracks = SAP_NTRACKS2;
+   else
+      num_tracks = SAP_NTRACKS1;
+
+   /* et de la taille du secteur */
+   if (density == 1)
+      sect_size = SAP_SECTSIZE2;
+   else
+      sect_size = SAP_SECTSIZE1;
+
+   /* construction de la carte des secteurs pour chaque piste,
+      à partir du facteur d'entrelacement */
+   BuildSectorMap(sector_map, factor);
+
+   /* formatage des pistes */
+   for (track=0; track<num_tracks; track++) {
+      if (verbose) {
+         printf("\r formatage piste %d", track);
+         fflush(stdout);
+      }
+
+      /* construction de la table des headers */
+      for (sect=1, pos=0; sect<=SAP_NSECTS; sect++, pos+=4) {
+         header_table[pos]   = track;
+         header_table[pos+1] = 0;
+         header_table[pos+2] = sector_map[sect-1];
+         header_table[pos+3] = density - 1;
+      }
+
+      if (FloppyFormatTrack(drive, density, track, header_table) != 0)
+         return 1;
+   }
+
+   if (verbose) {
+      printf("\n construction de la FAT (piste 20)");
+      fflush(stdout);
+   }
+
+   for (i=0; i<sect_size*SAP_NSECTS; i++)
+      buffer[i] = 0xFF;
+
+   /* pour MS-DOS (voir teo/src/dos/ddisk.c) */
+   buffer[i]=0;
+
+   if (FloppyWriteSector(drive, density, 20, 1, SAP_NSECTS, buffer) != 0)
+      return 2;
+
+   buffer[0] = 0;
+   buffer[41] = 0xFE;
+   buffer[42] = 0xFE;
+
+   /* pour MS-DOS (voir teo/src/dos/ddisk.c) */
+   buffer[sect_size] = 0;
+
+   if (FloppyWriteSector(drive, density, 20, 2, 1, buffer) != 0)
+      return 2;
+
+   if (verbose)
+      printf("\n");
+
+   return 0;
+}
+
+
+
+/* shrink_fat_160_to_80:
+ *  Réduit la taille de la FAT de 160 à 80 blocks.
+ */
+static void shrink_fat_160_to_80(unsigned char fat_data[])
+{
+   int i;
+
+   for (i=0; i<SAP_NTRACKS1; i++) {
+      if (fat_data[1 + SAP_NTRACKS1 + i] != 0xFF)
+         return;
+   }
+
+   memset(fat_data + 1 + SAP_NTRACKS1, 0xFE, SAP_NTRACKS1);
+}
+
+
+
+/* PackArchive:
+ *  Transfère le contenu d'une disquette TO dans une archive SAP.
+ */
+static int PackArchive(const char sap_name[], int drive, int density, int verbose)
+{
+   int num_tracks, sect_size;
+   int track, sect;
+   sapID sap_file;
+   sapsector_t sapsector;
+   unsigned char buffer[512*SAP_NSECTS];
+
+   if ((sap_file=sap_CreateArchive(sap_name, density == 1 ? SAP_FORMAT2 : SAP_FORMAT1)) == SAP_ERROR)
+      return 1;
+
+   /* détermination du nombre de pistes */
+   if (IS_5_INCHES(drive))
+      num_tracks = SAP_NTRACKS2;
+   else
+      num_tracks = SAP_NTRACKS1;
+
+   /* et de la taille du secteur */
+   if (density == 1)
+      sect_size = SAP_SECTSIZE2;
+   else
+      sect_size = SAP_SECTSIZE1;
+
+   for (track=0; track<num_tracks; track++) {
+      if (verbose) {
+         printf("\r lecture piste %d", track);
+         fflush(stdout);
+      }
+
+      if (FloppyReadSector(drive, density, track, 1, SAP_NSECTS, buffer) != 0) {
+         /* erreur dans la piste, on essaie secteur par secteur */
+         for (sect=1; sect<=SAP_NSECTS; sect++) {
+            sapsector.format     = 0;
+            sapsector.protection = 0;
+            sapsector.track      = track;
+            sapsector.sector     = sect;
+
+            if (FloppyReadSector(drive, density, track, sect, 1, buffer) != 0) {
+               /* erreur de lecture du secteur */
+               if (verbose)
+                  printf("\n   secteur %d illisible", sect);
+
+               sapsector.format = 4;
+               memset(sapsector.data, 0xF7, sect_size);
+            }
+            else {
+               memcpy(sapsector.data, buffer, sect_size);
+            }
+
+            /* traitement spécial de la FAT pour les disquettes 5"25 DD */
+            if ((track == 20) && (sect == 2) && IS_5_INCHES(drive) && (density == 2))
+               shrink_fat_160_to_80(sapsector.data);
+
+            sap_FillArchive(sap_file, &sapsector);
+         }
+
+         if ((track < num_tracks-1) && verbose)
+            printf("\n");
+      }
+      else {
+         for (sect=1; sect<=SAP_NSECTS; sect++) {
+            sapsector.format     = 0;
+            sapsector.protection = 0;
+            sapsector.track      = track;
+            sapsector.sector     = sect;
+
+            memcpy(sapsector.data, buffer+(sect-1)*sect_size, sect_size);
+
+            /* traitement spécial de la FAT pour les disquettes 5"25 DD */
+            if ((track == 20) && (sect == 2) && IS_5_INCHES(drive) && (density == 2))
+               shrink_fat_160_to_80(sapsector.data);
+
+            sap_FillArchive(sap_file, &sapsector);
+         }
+      }
+   }
+
+   if (verbose)
+      printf("\n");
+
+   sap_CloseArchive(sap_file);
+   return 0;
+}
+
+
+
+/* UnpackArchive:
+ *  Transfère le contenu d'une archive SAP vers une disquette TO.
+ */
+static int UnpackArchive(const char sap_name[], int drive, int density, int verbose)
+{
+   int format, num_tracks, sect_size;
+   int track, sect;
+   sapID sap_file;
+   sapsector_t sapsector;
+   unsigned char buffer[512*SAP_NSECTS];
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR)
+      return 1;
+
+   /* vérification du format */
+   if (((format == SAP_FORMAT1) && (density != 2)) || ((format == SAP_FORMAT2) && (density != 1)))
+      return 1;
+
+   /* détermination du nombre de pistes */
+   if (IS_5_INCHES(drive))
+      num_tracks = SAP_NTRACKS2;
+   else
+      num_tracks = SAP_NTRACKS1;
+
+   /* et de la taille du secteur */
+   if (density == 1)
+      sect_size = SAP_SECTSIZE2;
+   else
+      sect_size = SAP_SECTSIZE1;
+
+   for (track=0; track<num_tracks; track++) {
+      if (verbose) {
+         printf("\r %scriture piste %d", eacute, track);
+         fflush(stdout);
+      }
+
+      for (sect=1; sect<=SAP_NSECTS; sect++) {
+         sapsector.format     = 0;
+         sapsector.protection = 0;
+         sapsector.track      = track;
+         sapsector.sector     = sect;
+
+         if (sap_ReadSector(sap_file, track, sect, &sapsector) == SAP_ERROR) {
+            sap_CloseArchive(sap_file);
+            return 2;
+         }
+         else {
+            memcpy(buffer+(sect-1)*sect_size, sapsector.data, sect_size);
+         }
+      }
+
+      /* pour MS-DOS (voir teo/src/dos/ddisk.c) */
+      buffer[SAP_NSECTS*sect_size] = 0;
+
+      if (FloppyWriteSector(drive, density, track, 1, SAP_NSECTS, buffer) != 0) {
+         sap_CloseArchive(sap_file);
+         return 3;
+      }
+   }
+
+   if (verbose)
+      printf("\n");
+
+   sap_CloseArchive(sap_file);
+   return 0;
+}
+
+
+
+/* get_drive:
+ *  Helper pour obtenir le numéro de lecteur.
+ */
+static void get_drive(int *drive, int *density)
+{
+   char trash[8];
+   char* ret;
+
+   if (fi.num_drives > 1) {
+      do {
+         printf(" num%sro du lecteur: ", eacute);
+         if (!scanf("%d", drive))
+            ret=fgets(trash, 7, stdin);
+      }
+      while ((*drive<0) || (*drive>3) || (fi.drive_type[*drive]==0));
+   }
+   else {
+      for (*drive=0; *drive<4; (*drive)++) {
+         if (fi.drive_type[*drive] > 0)
+            break;
+      }
+   }
+
+   if (fi.fm_support && IS_5_INCHES(*drive)) {
+      do {
+         printf(" densit%s (1:simple, 2:double): ", eacute);
+         if (!scanf("%d", density))
+            ret=fgets(trash, 7, stdin);
+      }
+      while ((*density<1) || (*density>2));
+   }
+   else
+      *density = 2;
+}
+
+
+
+/* interactive_main:
+ *  Point d'entrée du programme interactif.
+ */
+static void interactive_main(void)
+{
+   int c, ret;
+   char trash[8];
+   int drive, density, format, factor;
+   char sap_name[FILENAME_LENGTH];
+   char* ret2;
+
+   while (1) {
+      printf("SAP2, syst%sme d'archivage pour disquettes 3\"5 et 5\"25 Thomson\n", egrave);
+      printf("version "SAP2_VERSION_STR" ("SAP2_PLATFORM_STR") copyright (C) 2000-2003 Eric Botcazou\n");
+      printf(" bas%s sur SAP copyright (C) Alexandre Pukall Avril 1998\n\n", eacute);
+
+      if (fi.drive_type[0] > 0) {
+         printf("Lecteur A: (%s) --> lecteur 0", drive_type_name[fi.drive_type[0]]);
+
+         if (fi.drive_type[1] > 0)
+            printf(" + lecteur 1\n");
+         else
+            printf("\n");
+      }
+      else {
+         printf("Lecteur A: (%s)\n", drive_type_name[0]);
+      }
+
+      if (fi.drive_type[2] > 0) {
+         printf("Lecteur B: (%s) --> lecteur 2", drive_type_name[fi.drive_type[2]]);
+
+         if (fi.drive_type[3] > 0)
+            printf(" + lecteur 3\n\n");
+         else
+            printf("\n\n");
+      }
+      else {
+         printf("Lecteur B: (%s)\n\n", drive_type_name[0]);
+      }
+
+      printf("Transfert TO-->PC:\n");
+      printf(" 1. Visualiser le contenu d'une disquette Thomson\n");
+      printf(" 2. Cr%ser une archive SAP vide\n", eacute);
+      printf(" 3. Archiver une disquette Thomson vers une archive SAP\n");
+      printf("Transfert PC-->TO:\n");
+      printf(" 4. Visualiser le contenu d'une archive SAP\n");
+      printf(" 5. Formater une disquette 3\"5 (720 ko) ou 5\"25 au format Thomson\n");
+      printf(" 6. D%ssarchiver une archive SAP vers une disquette Thomson\n", eacute);
+      printf("Autres commandes:\n");
+      printf(" 7. Quitter\n\n");
+
+      do {
+         printf("Votre choix: ");
+         if (!scanf("%d", &c))
+            ret2=fgets(trash, 7, stdin);
+      }
+      while ((c<1) || (c>7));
+
+      switch (c) {
+
+         case 1:
+            printf("Visualisation du contenu d'une disquette:\n");
+            get_drive(&drive, &density);
+            printf("\n");
+
+            ret = ViewDiskDir(drive, density, PAGE_HEIGHT);
+            if (ret == 1)
+               printf("*** Erreur: r%spertoire illisible ***\n", eacute);
+
+            break;
+
+         case 2:
+            printf("Cr%sation d'une archive vide:\n", eacute);
+
+            printf(" nom de l'archive %s cr%ser (sans extension): ", agrave, eacute);
+            ret=scanf("%s", sap_name);
+            strcat(sap_name, ".sap");
+
+            do {
+               printf(" format (1:5\"25 SD, 2:5\"25 DD, 3:3\"5 DD): ");
+               if (!scanf("%d", &format))
+                  ret2=fgets(trash, 7, stdin);
+            }
+            while ((format<1) || (format>3));
+
+            ret = CreateEmptyArchive(sap_name, (format == 1 ? SAP_FORMAT2 : SAP_FORMAT1), (format == 3 ? SAP_TRK80 : SAP_TRK40));
+            if (ret == 1)
+               printf("*** Erreur: impossible de cr%ser le fichier %s ***\n", eacute, sap_name);
+
+            break;
+
+         case 3:
+            printf("Archivage d'une disquette:\n");
+
+            printf(" nom de l'archive %s cr%ser (sans extension): ", agrave, eacute);
+            ret=scanf("%s", sap_name);
+            strcat(sap_name, ".sap");
+
+            get_drive(&drive, &density);
+            ret = PackArchive(sap_name, drive, density, 1);
+            if (ret == 1)
+               printf("*** Erreur: impossible de cr%ser le fichier %s ***\n", eacute, sap_name);
+
+            break;
+
+         case 4:
+            printf("Visualisation du contenu d'une archive:\n");
+
+            printf(" nom de l'archive (sans extension): ");
+            ret=scanf("%s", sap_name);
+            strcat(sap_name, ".sap");
+
+            printf("\n");
+
+            ret = ViewArchiveDir(sap_name, PAGE_HEIGHT);
+            if (ret == 1)
+               printf("*** Erreur: impossible d'ouvrir le fichier %s ***\n", sap_name);
+            else if (ret == 2)
+               printf("*** Erreur: archive SAP corrompue ***\n");
+
+            break;
+
+         case 5:
+            printf("Formatage d'une disquette au format Thomson:\n");
+            printf(" Si la disquette est une 3\"5 - 1.44 Mo, occulter l'encoche\n");
+            printf(" de droite (recto et verso) avec un morceau de scotch.\n");
+
+            get_drive(&drive, &density);
+   
+            do {
+               printf(" facteur d'entrelacement ([1..%d], %d recommand%s): ", SAP_NSECTS-1, (SAP_NSECTS-1)/2, eacute);
+               if (!scanf("%d", &factor))
+                  ret2=fgets(trash, 7, stdin);
+            }
+            while ((factor<1) || (factor>(SAP_NSECTS-1)));
+
+            ret = FormatDisk(drive, density, factor, 1);
+            if (ret == 1)
+               printf("\n*** Erreur: impossible de formater la disquette ***\n");
+            else if (ret == 2)
+               printf("*** Erreur: impossible d'%scrire sur la disquette ***\n", eacute);
+            break;
+
+         case 6:
+            printf("D%ssarchivage vers une disquette:\n", eacute);
+
+            get_drive(&drive, &density);
+
+            printf(" nom de l'archive (sans extension): ");
+            ret=scanf("%s", sap_name);
+            strcat(sap_name, ".sap");
+
+            ret = UnpackArchive(sap_name, drive, density, 1);
+            if (ret == 1)
+               printf("*** Erreur: impossible d'ouvrir le fichier %s ***\n", sap_name);
+            else if (ret == 2)
+               printf("*** Erreur: archive SAP corrompue ***\n");
+            else if (ret == 3)
+               printf("\n*** Erreur: impossible d'%scrire sur la disquette ***\n", eacute);
+            break;
+
+         case 7:
+            return;
+      }
+
+      ret2=fgets(trash, 7, stdin);
+      printf("Appuyer sur <Return>");
+      fflush(stdout);
+      getchar();
+      printf("\n");
+   }
+}
+
+
+#define COMMAND_MAX  8
+
+static char *short_command[] = {"-h", "-v", "-d", "-c", "-p", "-t", "-f", "-u"};
+static char *long_command[] = {"--help", "--version", "--dir", "--create", "--pack", "--list", "--format", "--unpack"};
+
+
+
+/* usage:
+ *  Affiche les commandes d'utilisation du programme et quitte.
+ */
+static void usage(const char prog_name[])
+{
+   fprintf(stderr, "Usage: %s  -h --help | -v --version | -t --list | -p --pack | -u --unpack\n", prog_name);
+   fprintf(stderr, "               -c --create | -d --dir | -f --format\n");
+   exit(EXIT_FAILURE);
+}
+
+
+
+/* main:
+ *  Point d'entrée du programme.
+ */
+int main(int argc, char *argv[])
+{
+   int ret = 0;
+   int i;
+
+   if (argc < 2) { /* no argument? */
+      if (FloppyInit(&fi, 1) > 0) {
+         interactive_main();
+         FloppyExit();
+      }
+      else {
+         ret = 1;
+      }
+   }
+   else {
+      if (argv[1][0] == '-') {
+
+         switch (argv[1][1]) {
+
+            case '-':  /* long commands */
+               for (i=0; i<COMMAND_MAX; i++) {
+                  if (strcmp(argv[1], long_command[i]) == 0) {
+                     argv[1] = short_command[i];
+                     return main(argc, argv);
+                  }
+               }
+
+               usage(argv[0]);
+
+            case 'h':  /* help */
+               printf("SAP2 est un syst%sme d'archivage pour disquettes 3\"5 et 5\"25 Thomson. Il permet\n", egrave);
+               printf("de cr%ser des images de disquettes sous forme d'archive SAP.\n\n", eacute);
+               printf("Usage:\n");
+               printf("    %s (mode interactif)\n", argv[0]);
+               printf("    %s commande1 archive.sap [lecteur] [densit%s]\n", argv[0], eacute);
+               printf("    %s commande2 archive.sap [nb pistes] [densit%s]\n", argv[0], eacute);
+               printf("    %s commande3 lecteur [densit%s] [entrelacement]\n", argv[0], eacute);
+               printf("o%s la commande1 est prise parmi les suivantes:\n", ugrave);
+               printf("  -h, --help          affiche cette aide\n");
+               printf("  -v, --version       affiche la version du programme\n");
+               printf("  -t, --list          affiche la liste des fichiers de l'archive SAP\n");
+               printf("  -p, --pack          archive une disquette Thomson vers une archive SAP\n");
+               printf("  -u, --unpack        d%ssarchive une archive SAP vers une disquette Thomson\n", eacute);
+               printf("et o%s la commande2 est prise parmi les suivantes:\n", ugrave);
+               printf("  -c, --create        cr%se une archive SAP vide\n", eacute);
+               printf("et o%s la commande3 est prise parmi les suivantes:\n", ugrave);
+               printf("  -d, --dir           affiche le contenu d'une disquette Thomson\n");
+               printf("  -f, --format        formate une disquette au format Thomson\n");
+               break;
+
+            case 'v':  /* version */
+               printf("SAP2 version "SAP2_VERSION_STR" pour "SAP2_PLATFORM_STR", copyright (C) 2000-2003 Eric Botcazou.\n");
+               break;
+
+            case 'd':  /* dir */
+               if (argc < 3)
+                  usage(argv[0]);
+
+               if (FloppyInit(&fi, 0) > 0) {
+                  ret = ViewDiskDir(atoi(argv[2]), argc > 3 ? atoi(argv[3]) : 2, -1);
+                  FloppyExit();
+               }
+               else {
+                  ret = 1;
+               }
+               break;
+
+            case 'c':  /* create */
+               if (argc < 3)
+                  usage(argv[0]);
+
+               if (argc < 4) {
+                  ret = CreateEmptyArchive(argv[2], SAP_FORMAT1, SAP_TRK80);
+               }
+               else if (argc == 4){
+                  if (atoi(argv[3]) == 40)
+                     ret = CreateEmptyArchive(argv[2], SAP_FORMAT1, SAP_TRK40);
+                  else if (atoi(argv[3]) == 80)
+                     ret = CreateEmptyArchive(argv[2], SAP_FORMAT1, SAP_TRK80);
+                  else
+                     ret = 1;
+               }
+               else {
+                  if (atoi(argv[3]) == 40)
+                     ret = CreateEmptyArchive(argv[2], (atoi(argv[4]) == 1 ? SAP_FORMAT2 : SAP_FORMAT1), SAP_TRK40);
+                  else if (atoi(argv[3]) == 80)
+                     ret = CreateEmptyArchive(argv[2], (atoi(argv[4]) == 1 ? SAP_FORMAT2 : SAP_FORMAT1), SAP_TRK80);
+                  else
+                     ret = 1;
+               }
+               break;
+
+            case 'p':  /* pack */
+               if (argc < 4)
+                  usage(argv[0]);
+
+               if (FloppyInit(&fi, 0) > 0) {
+                  ret = PackArchive(argv[2], atoi(argv[3]), argc > 4 ? atoi(argv[4]) : 2, 0);
+                  FloppyExit();
+               }
+               else {
+                  ret = 1;
+               }
+               break;
+
+            case 't':  /* list */
+               if (argc < 3)
+                  usage(argv[0]);
+
+               ret = ViewArchiveDir(argv[2], -1);
+               break;
+
+            case 'f':  /* format */
+               if (argc < 3)
+                  usage(argv[0]);
+
+               if (FloppyInit(&fi, 1) > 0) {
+                  if (argc == 3)
+                     ret = FormatDisk(atoi(argv[2]), 2, (SAP_NSECTS-1)/2, 0);
+                  else if (argc == 4)
+                     ret = FormatDisk(atoi(argv[2]), atoi(argv[3]), (SAP_NSECTS-1)/2, 0);
+                  else
+                     ret = FormatDisk(atoi(argv[2]), atoi(argv[3]), atoi(argv[4]), 0);
+
+                  FloppyExit();
+               }
+               else {
+                  ret = 1;
+               }
+               break;
+
+            case 'u':  /* unpack */
+               if (argc < 4)
+                  usage(argv[0]);
+
+               if (FloppyInit(&fi, 1) > 0) {
+                  ret = UnpackArchive(argv[2], atoi(argv[3]), argc > 4 ? atoi(argv[4]) : 2, 0);
+                  FloppyExit();
+               }
+               else {
+                  ret = 1;
+               }
+               break;
+
+            default:
+               usage(argv[0]);
+         }  /* end of switch */
+      }
+      else {
+         usage(argv[0]);
+      }
+   }
+
+   return ret;
+}
+
Index: tools/sap/sap2.txt
===================================================================
--- tools/sap/sap2.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/sap2.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,191 @@
+1. Introduction
+---------------
+Le système d'archivage SAP, conçu par Alexandre Pukall, est l'outil utilisé
+par les émulateurs Thomson pour manipuler de façon souple et sécurisée les
+disquettes 3"5 ou 5"25 au format Thomson. L'idée directrice de SAP est de
+reproduire les disquettes physiques Thomson sous forme de fichiers "images",
+stockables sur disque dur et lisibles directement par les émulateurs: toutes
+les opérations de lecture/écriture sont ainsi réalisées sur ces images et
+non pas sur les disquettes originales.
+
+Ce système permet de copier facilement les disquettes, de les diffuser par
+tout moyen numérique et assure in fine la préservation de leurs données au
+delà de la durée de vie du support physique.
+
+L'utilitaire SAP est le pont qui permet de faire le lien entre les deux
+modes de stockage. La version originale en a été écrite par Alexandre Pukall
+lui-même; la version étendue SAP2 présentée ci-dessous, disponible pour
+MSDOS et Linux, en reprend intégralement les fonctionnalités.
+
+
+2. Comment l'obtenir ?
+----------------------
+En le téléchargeant depuis la page Web:
+http://nostalgies.thomsonistes.org/utilitaires.html
+
+
+3. Fonctionnement du programme
+------------------------------
+SAP2 peut fonctionner selon deux modes: si aucun argument ne lui est passé
+sur la ligne de commandes, il entre en mode interactif et propose un menu des
+commandes disponibles; si au moins un argument lui est passé sur la ligne de
+commandes, il entre en mode batch et décode l'argument pour éxécuter la
+commande spécifiée.
+
+
+4. Commandes du programme en mode interactif
+--------------------------------------------
+Les commandes se répartissent en trois catégories: transfert TO-->PC,
+transfert PC-->TO et autres commandes.
+
+  4.1 Transfert TO-->PC
+  ---------------------
+  Commande 1: Visualiser le contenu d'une disquette Thomson
+    Cette commande peut être utile *avant* de lancer le processus
+    d'archivage proprement dit: elle affiche le répertoire d'une
+    disquette 3"5 ou 5"25 au format Thomson introduite dans un des
+    lecteurs (elle réalise l'équivalent de la commande DIR du BASIC).
+
+  Commande 2: Créer une archive SAP vide
+    Cette commande peut être éxécutée sans disquettes au format Thomson.
+    Elle crée une archive SAP vide et formatée (elle réalise l'équivalent
+    de la commande DSKINI du BASIC sur une archive SAP nouvellement créée),
+    soit au format 3"5 (80 pistes, 320 ko), soit au format 5"25 DD (40 pistes,
+    160 ko) ou 5"25 SD (40 pistes, 80 ko).
+
+  Commande 3: Archiver une disquette Thomson vers une archive SAP
+    Cette commande est la véritable commande d'archivage: elle lit le
+    contenu de la disquette 3"5 ou 5"25 au format Thomson introduite dans un
+    des lecteurs du PC et le restitue sous la forme d'une archive SAP sur le
+    disque dur du PC. Elle détecte les secteurs illisibles ou défectueux et
+    active le mécanisme que le format SAP définit dans ce cas.
+
+
+  4.2 Transfert PC-->TO
+  ---------------------
+  Commande 4: Visualiser le contenu d'une archive SAP
+    Cette commande peut être utile *avant* de lancer le processus de
+    désarchivage proprement dit: elle affiche la liste des fichiers
+    contenus dans l'archive SAP spécifiée. 
+
+  Commande 5: Formater une disquette 3"5 ou 5"25 au format Thomson
+    Le désarchivage (commande 6) d'une archive SAP vers une
+    disquette 3"5 ou 5"25 ne peut être réalisé que si la disquette est
+    au *préalable* formatée au format Thomson. Cette commande permet
+    d'appliquer ce formatage à une disquette 3"5 ou 5"25 introduite dans
+    un des lecteurs du PC; si la disquette est une disquette 3"5 - 1.44 Mo,
+    il est nécessaire d'occulter l'encoche de droite (recto et verso) avec
+    un morceau de scotch.
+
+  Commande 6: Désarchiver une archive SAP vers une disquette Thomson
+    Cette commande est la véritable commande de désarchivage: elle lit le
+    contenu d'une archive SAP et l'écrit sur la disquette 3"5 ou 5"25 au
+    format Thomson introduite dans un des lecteurs du PC. Attention: (voir
+    commande 5) la disquette doit être au *préalable* formatée au format
+    Thomson.
+
+
+  4.3 Autres commandes
+  --------------------
+  Commande 7: Quitter
+    Comme son nom ne l'indique pas ;-) elle permet de quitter le programme.
+
+
+5. Commandes du programme en mode batch
+---------------------------------------
+Une des commandes suivantes doit être passée à SAP2 comme premier argument:
+
+  -h, --help
+                    affiche l'aide en ligne du programme.
+                    ex:  sap2 --help
+
+  -v, --version
+                    affiche la version du programme.
+                    ex:  sap2 --version
+
+  -t, --list archive.sap
+                    affiche la liste des fichiers de l'archive SAP dont le
+                    nom complet est spécifié comme second argument.
+                    ex:  sap2 --list foo.sap
+                    [voir la commande 4 du mode interactif]
+
+  -p, --pack archive.sap lecteur [densité]
+                    archive une disquette Thomson vers une archive SAP dont
+                    le nom complet est spécifié comme deuxième argument; le
+                    numéro du lecteur dans lequel est inséré la disquette
+                    est spécifié comme troisième argument; si ce lecteur est
+                    un 5"25, la densité peut être spécifiée comme troisième
+                    argument (1 ou 2, 2 par défault).
+                    ex:  sap2 --pack foo.sap 0
+                    [voir la commande 3 du mode interactif]
+
+  -u, --unpack archive.sap lecteur [densité]
+                    désarchive une archive SAP, dont le nom complet est
+                    spécifié comme deuxième argument, vers une disquette
+                    Thomson insérée dans le lecteur dont le numéro est
+                    spécifié comme troisième argument; si ce lecteur est
+                    un 5"25, la densité peut être spécifiée comme troisième
+                    argument (1 ou 2, 2 par défault).
+                    ex:  sap2 --unpack foo.sap 0
+                    [voir la commande 6 du mode interactif]
+
+  -c, --create archive.sap [nb pistes] [densité]
+                    crée une archive SAP vide dont le nom complet est
+                    spécifié comme second argument; le format peut être
+                    spécifié comme troisième argument sous la forme du
+                    nombre de pistes (40 ou 80, 80 par défaut) et comme
+                    quatrième argument sous la forme de la densité (1 ou
+                    2, 2 par défault).
+                    ex:  sap2 --create foo.sap
+                         sap2 --create foo.sap 40
+                    [voir la commande 2 du mode interactif]
+
+  -d, --dir lecteur [densité]
+                    affiche le contenu de la disquette Thomson insérée dans
+                    le lecteur dont le numéro est spécifié comme second
+                    argument (0=lecteur A face 0, 1=lecteur A face 1,
+                    2=lecteur B face 0, 3=lecteur B face 1); si ce lecteur
+                    est un 5"25, la densité peut être spécifiée comme
+                    troisième argument (1 ou 2, 2 par défault).
+                    ex:  sap2 --dir 0
+                    [voir la commande 1 du mode interactif]
+
+  -f, --format lecteur [densité] [entrelacement]
+                    formate une disquette 3"5 ou 5"25 au format Thomson,
+                    insérée dans le lecteur dont le numéro est spécifié
+                    comme second argument; si ce lecteur est un 5"25, la
+                    densité peut être spécifiée comme troisième argument
+                    (1 ou 2, 2 par défault); le facteur d'entrelacement du
+                    formatage peut être spécifié comme quatrième argument
+                    (7 par défault).
+                    ex:  sap2 --format 0 6
+                         sap2 --format 1
+                    [voir la commande 5 du mode interactif]
+
+
+6. Problèmes connus
+-------------------
+- la version MSDOS peut fonctionner sous Windows 95 et 98; néanmoins, et
+  notamment si un logiciel anti-virus tourne en tâche de fond, il peut être
+  nécessaire de redémarrer l'ordinateur et d'ouvrir une session MS-DOS pure.
+- la version MSDOS de SAP ne permet pas d'archiver à partir ou de
+  désarchiver vers la face 1 d'une disquette Thomson; il s'agit d'une
+  impossibilité définitive dûe à une incompatibilité entre le DOS TO et
+  le BIOS PC. A contrario, la version Linux n'est pas concernée par ce
+  problème.
+- la version MSDOS de SAP ne permet pas d'accéder aux disquettes 5"25 simple
+  densité; il s'agit là encore d'une impossibilité définitive dûe aux
+  limitations du BIOS PC. A contrario, la version Linux n'est pas concernée
+  par ce problème.
+- il se peut que MSDOS ne puisse plus reformater au format standard une
+  disquette formatée au format Thomson.
+
+
+7. Conclusion
+-------------
+J'espère que cette version de SAP répondra à vos attentes; n'hésitez pas à
+me faire part de vos remarques et suggestions.
+
+
+Eric Botcazou
+e-mail: <ebotcazou@libertysurf.fr>
Index: tools/sap/sapfs.c
===================================================================
--- tools/sap/sapfs.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/sapfs.c	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,883 @@
+/*  SAPfs
+ *  Version 0.9.4
+ *  Copyright (C) 2001-2003 Eric Botcazou
+ *
+ *  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; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include "libsap.h"
+
+
+#define SAPFS_VERSION_STR "0.9.4"
+
+#ifdef linux
+   #define SAPFS_PLATFORM_STR "Linux"
+#else
+   #define SAPFS_PLATFORM_STR "MSDOS"
+#endif
+
+#define FILENAME_LENGTH 512
+
+
+/* ugly hack to support French accents */
+#ifdef linux
+static char eacute[] = "é";
+static char egrave[] = "è";
+static char agrave[] = "à";
+static char ugrave[] = "ù";
+#else
+static char eacute[] = "";
+static char egrave[] = "";
+static char agrave[] = "
+";
+static char ugrave[] = "";
+#endif
+
+
+
+/* PrintErrorMessage:
+ *  Prints the error message corresponding to the specified error.
+ */
+static void PrintErrorMessage(int errno, const char str[])
+{
+   switch (errno) {
+
+      case SAP_EBADF:
+         fprintf(stderr, "Erreur: le fichier %s n'est pas une archive SAP valide.\n", str);
+         break;
+
+      case SAP_EFBIG:
+         fprintf(stderr, "Erreur: le fichier %s est de taille trop importante.\n", str);
+         break;
+
+      case SAP_ENFILE:
+         fprintf(stderr, "Erreur: le fichier %s est vide.\n", str);
+         break;
+
+      case SAP_ENOENT:
+         fprintf(stderr, "Erreur: le fichier %s est introuvable.\n", str);
+         break;
+
+      case SAP_ENOSPC:
+         fprintf(stderr, "Erreur: le r%spertoire de l'archive est plein.\n", eacute);
+         break;
+
+      case SAP_EPERM:
+         fprintf(stderr, "Erreur: impossible de cr%ser le fichier %s.\n", eacute, str);
+         break;
+   }
+}
+
+
+
+/* put_separator:
+ *  Helper function to add a missing directory separator.
+ */
+static void put_separator(char filename[])
+{
+   int last;
+
+   last = strlen(filename) - 1;
+
+   if (filename[last] != '/')
+      filename[last + 1] = '/';
+}
+
+
+
+/* CreateEmptyArchive:
+ *  Creates a new archive using the specified name.
+ */
+static int CreateEmptyArchive(const char sap_name[], int format, int capacity)
+{
+   sapID sap_file;
+
+   if ((sap_file=sap_CreateArchive(sap_name, format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   sap_FormatArchive(sap_file, capacity);
+   sap_CloseArchive(sap_file);
+
+   return 0;
+}
+
+
+
+/* FormatArchive:
+ *  Formats the specified already existing archive.
+ */
+static int FormatArchive(const char sap_name[], int capacity)
+{
+   sapID sap_file;
+   int format;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   sap_FormatArchive(sap_file, capacity);
+   sap_CloseArchive(sap_file);
+
+   return 0;
+}
+
+
+
+/* VerifyArchive:
+ *  Verifies one or more sectors from the specified archive.
+ */
+static int VerifyArchive(const char sap_name[], int track, int sect)
+{
+   sapsector_t sapsector;
+   sapID sap_file;
+   int format, ntracks, s, t, flag;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   ntracks = (format == SAP_FORMAT1 ? SAP_NTRACKS1 : SAP_NTRACKS2);
+
+   /* check track number */
+   if (track<0) {
+      t = 0;
+      track = ntracks-1;
+   }
+   else {
+      if (track>=ntracks) {
+         fprintf(stderr, "Erreur: num%sro de piste invalide.\n", eacute);
+         sap_CloseArchive(sap_file);
+         return 1;
+      }
+
+      t = track;
+   }
+
+   /* check sector number */
+   if (sect<0) {
+      s = 1;
+      sect = SAP_NSECTS;
+   }
+   else {
+      if ((sect<1) || (sect>SAP_NSECTS)) {
+         fprintf(stderr, "Erreur: num%sro de secteur invalide.\n", eacute);
+         sap_CloseArchive(sap_file);
+         return 1;
+      }
+
+      s = sect;
+   }
+
+   while (t <= track) {
+      while (s <= sect) {
+         flag = sap_ReadSector(sap_file, t, s, &sapsector);
+
+         if (flag != SAP_OK) {
+            printf("track %d sector %02d: ", t, s);
+
+            if (flag & SAP_NO_STD_FMT)
+               printf("<format=%d> ", sapsector.format);
+
+            if (flag & SAP_PROTECTED)
+               printf("<protection=%d> ", sapsector.protection);
+
+            if (flag & SAP_BAD_SECTOR)
+               printf("<track=%d sector=%d> ", sapsector.track, sapsector.sector);
+
+            if (flag & SAP_CRC_ERROR)
+               printf("<CRC error>");
+
+            printf("\n");
+         }
+
+         s++;
+      }  /* end of while (s <= sect) */
+
+      s = 1;
+      t++;
+   }  /* end of while (t <= track ) */
+
+   sap_CloseArchive(sap_file);
+
+   return 0;
+}
+
+
+
+/* DumpArchive:
+ *  Dumps one or more sectors from the specified archive.
+ */
+static int DumpArchive(const char sap_name[], int track, int sect)
+{
+   sapsector_t sapsector;
+   unsigned char c;
+   sapID sap_file;
+   int format, ntracks, i,j,s,t;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   ntracks = (format == SAP_FORMAT1 ? SAP_NTRACKS1 : SAP_NTRACKS2);
+
+   /* check track number */
+   if (track<0) {
+      t = 0;
+      track = ntracks-1;
+   }
+   else {
+      if (track>=ntracks) {
+         fprintf(stderr, "Erreur: num%sro de piste invalide.\n", eacute);
+         sap_CloseArchive(sap_file);
+         return 1;
+      }
+
+      t = track;
+   }
+
+   /* check sector number */
+   if (sect<0) {
+      s = 1;
+      sect = SAP_NSECTS;
+   }
+   else {
+      if ((sect<1) || (sect>SAP_NSECTS)) {
+         fprintf(stderr, "Erreur: num%sro de secteur invalide.\n", eacute);
+         sap_CloseArchive(sap_file);
+         return 1;
+      }
+
+      s = sect;
+   }
+
+   while (t <= track) {
+      while (s <= sect) {
+         sap_ReadSector(sap_file, t, s, &sapsector);
+
+         printf("track id: %02d\n", sapsector.track);
+         printf("sector id: %02d\n", sapsector.sector);
+         printf("format: %d\n", sapsector.format);
+         printf("protection: %d\n", sapsector.protection);
+         printf("data:\n");
+
+         for (i=0; i< (format == SAP_FORMAT1 ? 16 : 8); i++) {
+            for (j=0; j<16; j++) {
+               c = sapsector.data[i*16+j];
+               printf("%02X ", c);
+            }
+
+            for (j=0; j<16; j++) {
+               c = sapsector.data[i*16+j];
+
+               if ((c>=32) && (c<=125))
+                  putchar(c);
+               else
+                  putchar('.');
+            }
+
+            printf("\n");
+         }
+
+         s++;
+      }  /* end of while (s <= sect) */
+
+      s = 1;
+      t++;
+   }  /* end of while (t <= track ) */
+
+   sap_CloseArchive(sap_file);
+
+   return 0;
+}
+
+
+
+/* ListArchive:
+ *  Lists the files contained in the specified archive.
+ */
+static int ListArchive(const char sap_name[])
+{
+   char buffer[4096];
+   sapID sap_file;
+   int format;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   sap_ListArchive(sap_file, buffer, sizeof(buffer));
+   puts(buffer);
+   sap_CloseArchive(sap_file);
+
+   return 0;
+}
+
+
+
+/* PrintFileInfo:
+ *  Prints the info for the specified file.
+ */
+static int PrintFileInfo(const char sap_name[], char filename[])
+{
+   int format, i, ret = 0;
+   sapID sap_file;
+   sapfileinfo_t info;
+   char tmp[64];
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   if (sap_GetFileInfo(sap_file, filename, &info) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, filename);
+      ret = 1;
+   }
+   else {
+      printf("name: %s\n", filename);
+      printf("size: %d bytes\n", info.size);
+      printf("file type: %d\n", info.file_type);
+      printf("data type: %d\n", info.data_type);
+
+      if (info.date) {
+         strftime(tmp, sizeof(tmp), "%m/%d/%Y %H:%M ", localtime(&info.date));
+         printf("date: %s\n", tmp);
+      }
+
+      if (info.comment[0])
+         printf("comment: %s\n", info.comment);
+
+      printf("blocks: ");
+      for (i=0; i<info.nblocks; i++)
+         printf("%d ", info.block[i]);
+      printf("\n");
+
+      free(info.block);
+   }
+
+   sap_CloseArchive(sap_file);
+
+   return ret;
+}
+
+
+
+/* ExtractFile:
+ *  Extracts one or more files from the specified archive.
+ */
+static int ExtractFile(const char sap_name[], char *filename[], int nfiles)
+{
+   int format, i, len, ret = 0;
+   sapID sap_file;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   for (i=0; i<nfiles; i++) {
+      len = sap_ExtractFile(sap_file, filename[i]);
+
+      if (len == 0) {
+         PrintErrorMessage(sap_errno, filename[i]);
+         ret = 1;
+         break;
+      }
+   }
+
+   sap_CloseArchive(sap_file);
+
+   return ret;
+}
+
+
+
+/* AddFile:
+ *  Adds one or more files to the specified archive.
+ */
+static int AddFile(const char sap_name[], char *filename[], int nfiles)
+{
+   char pathname[FILENAME_LENGTH];
+   int format, i, len = 0, ret = 0;
+   struct stat s;
+   struct dirent *entry;
+   sapID sap_file;
+   DIR *dir;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   for (i=0; i<nfiles; i++) {
+      /* get file statistics */
+      if (stat(filename[i], &s) != 0) {
+         fprintf(stderr, "Erreur: le fichier %s est introuvable.\n", filename[i]);
+         ret = 1;
+         break;
+      }
+
+      if (S_ISDIR(s.st_mode)) {  /* directory? */
+         if ((dir=opendir(filename[i])) == NULL) {
+            fprintf(stderr, "Erreur: le r%spertoire %s est inaccessible en lecture.\n", eacute, filename[i]);
+            ret = 1;
+            break;
+         }
+
+         /* add every entry in turn */
+         while ((entry = readdir(dir)) != NULL) {
+            if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) {
+               strncpy(pathname, filename[i], FILENAME_LENGTH - 1);
+               put_separator(pathname);
+               strncat(pathname, entry->d_name, FILENAME_LENGTH - strlen(pathname) - 1);
+               len = sap_AddFile(sap_file, pathname);
+
+               if (len == 0) {
+                  PrintErrorMessage(sap_errno, pathname);
+                  ret = 1;
+                  break;
+               }
+            }
+         }
+
+         closedir(dir);
+      }
+      else {
+         len = sap_AddFile(sap_file, filename[i]);
+      }
+
+      if (len == 0) {
+         if (ret == 0) {
+            PrintErrorMessage(sap_errno, filename[i]);
+            ret = 1;
+         }
+         break;
+      }
+   }
+
+   sap_CloseArchive(sap_file);
+
+   return ret;
+}
+
+
+
+/* DeleteFile:
+ *  Deletes one or more files from the specified archive.
+ */
+static int DeleteFile(const char sap_name[], char *filename[], int nfiles)
+{
+   int format, i, len, ret = 0;
+   sapID sap_file;
+
+   if ((sap_file=sap_OpenArchive(sap_name, &format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, sap_name);
+      return 1;
+   }
+
+   for (i=0; i<nfiles; i++) {
+      len = sap_DeleteFile(sap_file, filename[i]);
+
+      if (len == 0) {
+         PrintErrorMessage(sap_errno, filename[i]);
+         ret = 1;
+         break;
+      }
+   }
+
+   sap_CloseArchive(sap_file);
+
+   return ret;
+}
+
+
+
+/* CopyArchive:
+ *  Copies one or more sectors from the source archive to the dest archive.
+ */
+static int CopyArchive(const char src_name[], const char dest_name[], int track, int sect)
+{
+   sapsector_t sapsector;
+   sapID src_file, dest_file;
+   int src_format, dest_format, ntracks, s, t;
+
+   if ((src_file=sap_OpenArchive(src_name, &src_format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, src_name);
+      return 1;
+   }
+
+   if ((dest_file=sap_OpenArchive(dest_name, &dest_format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, dest_name);
+      sap_CloseArchive(src_file);
+      return 1;
+   }
+
+   if (src_format != dest_format) {
+      fprintf(stderr, "Erreur: archives de format diff%srent.\n", eacute);
+      goto Error;
+   }
+
+   ntracks = (src_format == SAP_FORMAT1 ? SAP_NTRACKS1 : SAP_NTRACKS2);
+
+   /* check track number */
+   if (track<0) {
+      t = 0;
+      track = ntracks-1;
+   }
+   else {
+      if (track>=ntracks) {
+         fprintf(stderr, "Erreur: num%sro de piste invalide.\n", eacute);
+         goto Error;
+      }
+
+      t = track;
+   }
+
+   /* check sector number */
+   if (sect<0) {
+      s = 1;
+      sect = SAP_NSECTS;
+   }
+   else {
+      if ((sect<1) || (sect>SAP_NSECTS)) {
+         fprintf(stderr, "Erreur: num%sro de secteur invalide.\n", eacute);
+         goto Error;
+      }
+
+      s = sect;
+   }
+
+   while (t <= track) {
+      while (s <= sect) {
+         sap_ReadSector(src_file, t, s, &sapsector);
+         sap_WriteSector(dest_file, t, s, &sapsector);
+
+         s++;
+      }  /* end of while (s <= sect) */
+
+      s = 1;
+      t++;
+   }  /* end of while (t <= track ) */
+
+   sap_CloseArchive(dest_file);
+   sap_CloseArchive(src_file);
+   return 0;
+
+ Error:
+   sap_CloseArchive(src_file);
+   sap_CloseArchive(dest_file);
+   return 1;
+}
+
+
+
+/* MoveSector:
+ *  Copies with deplacement one sector from the source archive to the dest archive.
+ */
+static int MoveSector(const char src_name[], int src_track, int src_sect,
+                        const char dest_name[], int dest_track, int dest_sect)
+{
+   sapsector_t sapsector;
+   sapID src_file, dest_file;
+   int src_format, dest_format, ntracks;
+
+   if ((src_file=sap_OpenArchive(src_name, &src_format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, src_name);
+      return 1;
+   }
+
+   if ((dest_file=sap_OpenArchive(dest_name, &dest_format)) == SAP_ERROR) {
+      PrintErrorMessage(sap_errno, dest_name);
+      sap_CloseArchive(src_file);
+      return 1;
+   }
+
+   if (src_format != dest_format) {
+      fprintf(stderr, "Erreur: archives de format diff%srent.\n", eacute);
+      goto Error;
+   }
+
+   ntracks = (src_format == SAP_FORMAT1 ? SAP_NTRACKS1 : SAP_NTRACKS2);
+
+   /* check track number */
+   if ((src_track<0) || (src_track>=ntracks)) {
+      fprintf(stderr, "Erreur: num%sro de piste invalide.\n", eacute);
+      goto Error;
+   }
+
+   /* check sector number */
+   if ((src_sect<1) || (src_sect>SAP_NSECTS)) {
+      fprintf(stderr, "Erreur: num%sro de secteur invalide.\n", eacute);
+      goto Error;
+   }
+
+   /* check track number */
+   if ((dest_track<0) || (dest_track>=ntracks)) {
+      fprintf(stderr, "Erreur: num%sro de piste invalide.\n", eacute);
+      goto Error;
+   }
+
+   /* check sector number */
+   if ((dest_sect<1) || (dest_sect>SAP_NSECTS)) {
+      fprintf(stderr, "Erreur: num%sro de secteur invalide.\n", eacute);
+      goto Error;
+   }
+
+   sap_ReadSector(src_file, src_track, src_sect, &sapsector);
+
+   sapsector.track = dest_track;
+   sapsector.sector = dest_sect;
+
+   sap_WriteSector(dest_file, dest_track, dest_sect, &sapsector);
+
+   sap_CloseArchive(src_file);
+   sap_CloseArchive(dest_file);
+   return 0;
+
+ Error:
+   sap_CloseArchive(src_file);
+   sap_CloseArchive(dest_file);
+   return 1;
+}
+
+
+#define COMMAND_MAX  14
+
+static char *short_command[] = { "-h", "-v", "-c", "-f", "-w", "-u",
+                                 "-t", "-i", "-x", "-y", "-a", "-d", "-k", "-m" };
+static char *long_command[] = { "--help", "--version", "--create", "--format", "--verify", "--dump",
+                                "--list", "--info", "--extract", "--extract-all", "--add", "--delete",
+                                "--copy", "--move" };
+
+
+/* usage:
+ *  Displays the commands and quits.
+ */
+static void usage(const char prog_name[])
+{
+   fprintf(stderr, "Usage: %s -h --help | -v --version | -c --create |  -f --format\n", prog_name);
+   fprintf(stderr, "               -w --verify | -d --dump | -t --list | -i --info\n");
+   fprintf(stderr, "               -x --extract[-all] | -a --add | -d --delete\n");
+   exit(EXIT_FAILURE);
+}
+
+
+
+/* main:
+ *  Entry point for the program.
+ */
+int main(int argc, char *argv[])
+{
+   int i, ret = 0;
+   char *star = "*";
+
+   if (argc < 2)  /* no argument? */
+      usage(argv[0]);
+
+   if (argv[1][0] == '-') {
+
+      switch (argv[1][1]) {
+
+         case '-':  /* long commands */
+            for (i=0; i<COMMAND_MAX; i++) {
+               if (strcmp(argv[1], long_command[i]) == 0) {
+                  argv[1] = short_command[i];
+                  return main(argc, argv);
+               }
+            }
+
+            usage(argv[0]);
+
+         case 'h':  /* help */
+            printf("SAPfs est un outil de manipulation des archives SAP qui permet de r%saliser\n", eacute);
+            printf("sur ces archives les op%srations naturelles d'un syst%sme de fichiers.\n\n", eacute, egrave);
+            printf("Usage:\n");
+            printf("    %s commande1 archive.sap [fichier...] [piste] [sect]\n", argv[0]);
+            printf("    %s commande2 archive.sap [nb pistes] [densit%s]\n", argv[0], eacute);
+            printf("    %s commande3 archive.sap archive2.sap [piste] [sect]\n", argv[0]);
+            printf("    %s commande4 archive1.sap piste sect archive2.sap piste sect\n", argv[0]);
+            printf("o%s la commande1 est prise parmi les suivantes:\n", ugrave);
+            printf("  -h, --help          affiche cette aide\n");
+            printf("  -v, --version       affiche la version du programme\n");
+            printf("  -w, --verify        effectue une v%srification d'un ou plusieurs secteurs\n", eacute);
+            printf("  -u, --dump          affiche le contenu d'un ou plusieurs secteurs\n");
+            printf("  -t, --list          affiche la liste des fichiers de l'archive SAP\n");
+            printf("  -i, --info          affiche les informations relatives %s un fichier\n", agrave);
+            printf("  -x, --extract       extrait un ou plusieurs fichiers de l'achive SAP\n");
+            printf("      --extract-all   extrait tous les fichiers de l'archive SAP\n");
+            printf("  -a, --add           ajoute un ou plusieurs fichiers %s l'archive SAP\n", agrave);
+            printf("  -d, --delete        d%struit un ou plusieurs fichiers de l'archive SAP\n", eacute);
+            printf("et o%s la commande2 est prise parmi les suivantes:\n", ugrave);
+            printf("  -c, --create        cr%se une archive SAP vide\n", eacute);
+            printf("  -f, --format        formate une archive SAP\n");
+            printf("et o%s la commande3 est prise parmi les suivantes:\n", ugrave);
+            printf("  -k, --copy          copie un ou plusieurs secteurs\n");
+            printf("et o%s la commande4 est prise parmi les suivantes:\n", ugrave);
+            printf("  -m, --move          copie un secteur avec d%splacement\n", eacute);
+            break;
+
+         case 'v':  /* version */
+            printf("SAPfs version "SAPFS_VERSION_STR" pour "SAPFS_PLATFORM_STR", copyright (C) 2001-2003 Eric Botcazou.\n");
+            break;
+
+         case 'c':  /* create */
+            if (argc < 3)
+               usage(argv[0]);
+
+            if (argc < 4) {
+               ret = CreateEmptyArchive(argv[2], SAP_FORMAT1, SAP_TRK80);
+            }
+            else if (argc == 4){
+               if (atoi(argv[3]) == 40)
+                  ret = CreateEmptyArchive(argv[2], SAP_FORMAT1, SAP_TRK40);
+               else if (atoi(argv[3]) == 80)
+                  ret = CreateEmptyArchive(argv[2], SAP_FORMAT1, SAP_TRK80);
+               else
+                  ret = 1;
+            }
+            else {
+               if (atoi(argv[3]) == 40)
+                  ret = CreateEmptyArchive(argv[2], (atoi(argv[4]) == 1 ? SAP_FORMAT2 : SAP_FORMAT1), SAP_TRK40);
+               else if (atoi(argv[3]) == 80)
+                  ret = CreateEmptyArchive(argv[2], (atoi(argv[4]) == 1 ? SAP_FORMAT2 : SAP_FORMAT1), SAP_TRK80);
+               else
+                  ret = 1;
+            }
+            break;
+
+         case 'f':  /* format */
+            if (argc < 3)
+               usage(argv[0]);
+
+            if (argc < 4) {
+               ret = FormatArchive(argv[2], SAP_TRK80);
+            }
+            else {
+               if (atoi(argv[3]) == 80)
+                  ret = FormatArchive(argv[2], SAP_TRK80);
+               else if (atoi(argv[3]) == 40)
+                  ret = FormatArchive(argv[2], SAP_TRK40);
+               else
+                  ret = 1;
+            }
+            break;
+
+         case 'w':  /* verify */
+            if (argc < 3)
+               usage(argv[0]);
+
+            if (argc < 4)
+               ret = VerifyArchive(argv[2], -1, -1);
+            else if (argc < 5)
+               ret = VerifyArchive(argv[2], atoi(argv[3]), -1);
+            else
+               ret = VerifyArchive(argv[2], atoi(argv[3]), atoi(argv[4]));
+            break;
+
+
+         case 'u':  /* dump */
+            if (argc < 3)
+               usage(argv[0]);
+
+            if (argc < 4)
+               ret = DumpArchive(argv[2], -1, -1);
+            else if (argc < 5)
+               ret = DumpArchive(argv[2], atoi(argv[3]), -1);
+            else
+               ret = DumpArchive(argv[2], atoi(argv[3]), atoi(argv[4]));
+            break;
+
+         case 't':  /* list */
+            if (argc < 3)
+               usage(argv[0]);
+
+            ret = ListArchive(argv[2]);
+            break;
+
+         case 'i':  /* info */
+            if (argc < 4)
+               usage(argv[0]);
+
+            ret = PrintFileInfo(argv[2], argv[3]);
+            break;
+
+         case 'x':  /* extract */
+            if (argc < 4)
+               usage(argv[0]);
+
+            ret = ExtractFile(argv[2], argv+3, argc-3);
+            break;
+
+         case 'y':  /* extract all */
+            if (argc < 3)
+               usage(argv[0]);
+
+            ret = ExtractFile(argv[2], &star, 1);
+            break;
+
+         case 'a':  /* add */
+            if (argc < 4)
+               usage(argv[0]);
+
+            ret = AddFile(argv[2], argv+3, argc-3);
+            break;
+
+         case 'd':  /* delete */
+            if (argc < 4)
+               usage(argv[0]);
+
+            ret = DeleteFile(argv[2], argv+3, argc-3);
+            break;
+
+         case 'k':  /* copy */
+            if (argc < 4)
+               usage(argv[0]);
+
+            if (argc < 5)
+               ret = CopyArchive(argv[2], argv[3], -1, -1);
+            else if (argc < 6)
+               ret = CopyArchive(argv[2], argv[3], atoi(argv[4]), -1);
+            else
+               ret = CopyArchive(argv[2], argv[3], atoi(argv[4]), atoi(argv[5]));
+            break;
+
+         case 'm':  /* move */
+            if (argc != 8)
+               usage(argv[0]);
+
+            ret = MoveSector(argv[2], atoi(argv[3]), atoi(argv[4]), argv[5], atoi(argv[6]), atoi(argv[7]));
+            break;
+
+         default:
+            usage(argv[0]);
+      }  /* end of switch */
+   }
+
+   return ret;
+}
+
Index: tools/sap/sapfs.txt
===================================================================
--- tools/sap/sapfs.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
+++ tools/sap/sapfs.txt	(revision 134f1c41158fa9fc8f7c9989267c79a63e7bcfec)
@@ -0,0 +1,166 @@
+1. Introduction
+---------------
+Le système d'archivage SAP, conçu par Alexandre Pukall, est l'outil utilisé
+par les émulateurs Thomson pour manipuler de façon souple et sécurisée les
+disquettes 3"5 ou 5"25 au format Thomson. L'idée directrice de SAP est de
+reproduire les disquettes physiques Thomson sous forme de fichiers "images",
+stockables sur disque dur et lisibles directement par les émulateurs: toutes
+les opérations de lecture/écriture sont ainsi réalisées sur ces images et
+non pas sur les disquettes originales.
+
+Ce système permet de copier facilement les disquettes, de les diffuser par
+tout moyen numérique et assure in fine la préservation de leurs données au
+delà de la durée de vie du support physique.
+
+L'utilitaire SAPfs permet de réaliser sur les archives SAP les opérations
+naturelles d'un système de fichiers: création, formatage, vérification, dump
+binaire, listage des fichiers, extraction de fichiers, ajout de fichiers et
+destruction de fichiers.
+
+
+2. Comment l'obtenir ?
+----------------------
+En le téléchargeant depuis la page Web:
+http://nostalgies.thomsonistes.org/utilitaires.html
+
+
+3. Fonctionnement du programme
+------------------------------
+SAPfs fonctionne en mode batch: une commande éventuellement suivie d'un ou
+plusieurs arguments doit lui être passée sur la ligne de commandes.
+
+
+4. Commandes du programme
+-------------------------
+Une des commandes suivantes doit être passée à SAPfs comme premier argument:
+
+  -h, --help
+                    affiche l'aide en ligne du programme.
+                    ex:  sapfs --help
+
+  -v, --version
+                    affiche la version du programme.
+                    ex:  sapfs --version
+
+  -w, --verify archive.sap [piste] [secteur]
+                    vérifie l'archive SAP dont le nom complet est spécifié
+                    comme second argument et affiche le détail des erreurs
+                    pour chaque secteur défectueux; si un troisième argument
+                    est spécifié, il est interprété comme le numéro de piste
+                    à laquelle la vérification sera restreinte; si un
+                    quatrième argument est spécifié, il est interprété comme
+                    le numéro de secteur.
+                    ex:  sapfs --verify foo.sap
+                         sapfs --verify foo.sap 10
+
+  -u, --dump archive.sap [piste] [secteur]
+                    effectue un dump binaire de l'archive SAP dont le nom
+                    complet est spécifié comme second argument; si un
+                    troisième argument est spécifié, il est interprété
+                    comme le numéro de piste à laquelle le dump sera
+                    restreint; si un quatrième argument est spécifié, il
+                    est interprété comme le numéro de secteur.
+                    ex:  sapfs --dump foo.sap 20 2
+                         sapfs --dump foo.sap 10
+
+  -t, --list archive.sap
+                    affiche la liste des fichiers de l'archive SAP dont le
+                    nom complet est spécifié comme second argument.
+                    ex:  sapfs --list foo.sap
+
+  -i, --info archive.sap fichier
+                    affiche les informations relatives à un fichier de 
+                    l'archive SAP dont le nom complet est spécifié comme 
+                    second argument; le nom de ce fichier est spécifié
+                    comme troisième argument; la casse est prise en compte.
+                    ex:  sapfs --info foo.sap AUTO.BAT
+
+  -x, --extract archive.sap fichier [fichier...]
+                    extrait un ou plusieurs fichiers de l'archive SAP dont
+                    le nom complet est spécifié comme second argument; le
+                    nom de ce(s) fichier(s) est spécifié comme troisième
+                    argument (puis quatrième, cinquième, etc); le nom de
+                    fichier peut contenir les wildcards '*' et '?'; la
+                    casse est prise en compte.
+                    ex:  sapfs --extract foo.sap BAR.BIN
+                         sapfs --extract foo.sap Prog.BAS Prog.BIN
+                         sapfs --extract foo.sap *.DAT
+
+      --extract-all archive.sap
+                    extrait tous les fichiers de l'archive SAP dont le nom
+                    est spécifié comme second argument.
+                    ex:  sapfs --extract-all foo.sap
+
+  -a, --add archive.sap fichier [fichier...] [répertoire...]
+                    ajoute un ou plusieurs fichiers à l'archive SAP dont le
+                    nom est spécifié comme second argument; le nom de ce(s)
+                    fichier(s) est spécifié comme troisième argument (puis
+                    quatrième, cinquième, etc); si un nom de répertoire est
+                    spécifié, tous les fichiers qui y sont contenus seront
+                    ajoutés; la casse est prise en compte.
+                    ex:  sapfs --add foo.sap BAR.BIN
+                         sapfs --add foo.sap Prog.BAS Prog.BIN
+
+  -d, --delete archive.sap fichier [fichier...]
+                    détruit un ou plusieurs fichiers de l'archive SAP dont
+                    le nom complet est spécifié comme second argument; le
+                    nom de ce(s) fichier(s) est spécifié comme troisième
+                    argument (puis quatrième, cinquième, etc); le nom de
+                    fichier peut contenir les wildcards '*' et '?'; la
+                    casse est prise en compte.
+                    ex:  sapfs --delete foo.sap B?R.BIN
+                         sapfs --delete foo.sap thom.bas son.bas
+                         sapfs --delete foo.sap LEVEL*.DAT
+
+  -c, --create archive.sap [nb pistes] [densité]
+                    crée une archive SAP vide (mais formatée) dont le nom
+                    complet est spécifié comme second argument; le format
+                    peut être spécifié comme troisième argument sous la
+                    forme du nombre de pistes (40 ou 80, 80 par défaut) et
+                    comme quatrième argument sous la forme de la densité
+                    (1 ou 2, 2 par défault).
+                    ex:  sapfs --create foo.sap
+                    ex:  sapfs --create foo.sap 40
+
+  -f, --format archive.sap [nb pistes] [densité]
+                    formate l'archive SAP dont le nom complet est spécifié
+                    comme second argument;; le format peut être spécifié
+                    comme troisième argument sous la forme du nombre de
+                    pistes (40 ou 80, 80 par défaut) et comme quatrième
+                    argument sous la forme de la densité (1 ou 2, 2 par
+                    défault).
+                    ex:  sapfs --format foo.sap
+                    ex:  sapfs --format foo.sap 40
+
+  -k, --copy archive1.sap archive2.sap [piste] [secteur]
+                    copie le contenu de l'archive source dont le nom complet
+                    est spécifié comme second argument vers l'archive
+                    destination dont le nom complet est spécifié comme
+                    troisième argument; si un quatrième argument est
+                    spécifié, il est interprété comme le numéro de piste à
+                    laquelle la copie sera restreinte; si un cinquième
+                    argument est spécifié, il est interprété comme le numéro
+                    de secteur.
+                    ex:  sapfs --copy foo.sap bar.sap 65
+                         sapfs --copy foo.sap bar.sap 20 3
+
+  -m, --move archive1.sap piste secteur archive2.sap piste secteur
+                    copie le contenu du secteur repéré par les trois premiers
+                    arguments vers le secteur repéré par les trois derniers
+                    arguments.
+                    ex:  sapfs --move foo.sap 12 4 bar.sap 26 16
+
+
+5. Problèmes connus
+-------------------
+Aucun pour le moment (je croise les doigts ;-)
+
+
+6. Conclusion
+-------------
+J'espère que cette version de SAPfs répondra à vos attentes; n'hésitez pas à
+me faire part de vos remarques et suggestions.
+
+
+Eric Botcazou
+e-mail: <ebotcazou@libertysurf.fr>
