547 lines
13 KiB
C
547 lines
13 KiB
C
|
/*----------------------------------------------------------------------------
|
||
|
* pgsparse - Parses BluRay PGS/SUP files
|
||
|
* Copyright (C) 2010 Arne Bochem <avs2bdnxml at ps-auxw de>
|
||
|
*
|
||
|
* 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 3 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, see <http://www.gnu.org/licenses/>.
|
||
|
*----------------------------------------------------------------------------*/
|
||
|
|
||
|
#include <stdint.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <stdio.h>
|
||
|
#include <string.h>
|
||
|
#include <math.h>
|
||
|
|
||
|
#ifdef BE_ARCH
|
||
|
#define SWAP32(x) (x)
|
||
|
#define SWAP16(x) (x)
|
||
|
#endif
|
||
|
#ifdef LE_ARCH
|
||
|
#define SWAP32(x) ((int32_t)(((x & 0xff000000) >> 24) | ((x & 0xff0000) >> 8) | ((x & 0xff00) << 8) | ((x & 0xff) << 24)))
|
||
|
#define SWAP16(x) ((int16_t)(((x & 0xff) << 8) | ((x & 0xff00) >> 8)))
|
||
|
#endif
|
||
|
#ifndef LE_ARCH
|
||
|
#ifndef BE_ARCH
|
||
|
#error "Please specify endian-ness with -DLE_ARCH or -DBE_ARCH."
|
||
|
#endif
|
||
|
#endif
|
||
|
|
||
|
#define MAX(a,b) ((a) > (b) ? (a) : (b))
|
||
|
#define MIN(a,b) ((a) < (b) ? (a) : (b))
|
||
|
|
||
|
typedef struct parser_state_s
|
||
|
{
|
||
|
FILE *fh;
|
||
|
int total_object_sizes;
|
||
|
int images;
|
||
|
int palettes;
|
||
|
int diff_palettes;
|
||
|
char last_pal[256 * 5];
|
||
|
} parser_state_t;
|
||
|
|
||
|
void clear_palette (parser_state_t *ps)
|
||
|
{
|
||
|
memset(ps->last_pal, 0, 256*5);
|
||
|
}
|
||
|
|
||
|
void clear_stats (parser_state_t *ps)
|
||
|
{
|
||
|
ps->total_object_sizes = 0;
|
||
|
ps->images = 0;
|
||
|
ps->palettes = 0;
|
||
|
ps->diff_palettes = 0;
|
||
|
}
|
||
|
|
||
|
parser_state_t *new_sup_parser (char *filename)
|
||
|
{
|
||
|
parser_state_t *ps = malloc(sizeof(parser_state_t));
|
||
|
|
||
|
if ((ps->fh = fopen(filename, "rb")) == NULL)
|
||
|
{
|
||
|
fprintf(stderr, "Couldn't open SUP file (%s): ", filename);
|
||
|
perror(NULL);
|
||
|
abort();
|
||
|
}
|
||
|
|
||
|
clear_stats(ps);
|
||
|
clear_palette(ps);
|
||
|
|
||
|
return ps;
|
||
|
}
|
||
|
|
||
|
void die (FILE *fh, int offset, char *error)
|
||
|
{
|
||
|
fprintf(stderr, "Error at %08x: %s\n", (int)ftell(fh) - offset, error);
|
||
|
exit(1);
|
||
|
}
|
||
|
|
||
|
void safe_read (void *p, int size, FILE *fh, char *desc)
|
||
|
{
|
||
|
char *error;
|
||
|
int r;
|
||
|
|
||
|
if ((r = fread(p, 1, size, fh)) != size)
|
||
|
{
|
||
|
error = malloc(128);
|
||
|
error[127] = 0;
|
||
|
snprintf(error, 127, "Could not read all %uB of %s.", size, desc);
|
||
|
die(fh, r, error);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void safe_seek (FILE *fh, int pos, char *desc)
|
||
|
{
|
||
|
char *error;
|
||
|
|
||
|
if (fseek(fh, pos, SEEK_CUR))
|
||
|
{
|
||
|
error = malloc(128);
|
||
|
error[127] = 0;
|
||
|
snprintf(error, 127, "Unexpected seek failure during %s.", desc);
|
||
|
perror(error);
|
||
|
abort();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
typedef struct fps_id_s
|
||
|
{
|
||
|
int num;
|
||
|
int den;
|
||
|
int id;
|
||
|
} fps_id_t;
|
||
|
|
||
|
int get_fps (int id, int *fps_num, int *fps_den)
|
||
|
{
|
||
|
fps_id_t ids[] = {{24000, 1001, 16}, {24, 1, 32}, {25, 1, 48}, {30000, 1001, 64}, {50, 1, 96}, {60000, 1001, 112}, {0, 0, 0}};
|
||
|
int i = 0;
|
||
|
|
||
|
while (ids[i].id)
|
||
|
{
|
||
|
if (ids[i].id == id)
|
||
|
{
|
||
|
*fps_num = ids[i].num;
|
||
|
*fps_den = ids[i].den;
|
||
|
return 1;
|
||
|
}
|
||
|
i++;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
typedef struct sup_header_s
|
||
|
{
|
||
|
uint8_t m1; /* 'P' */
|
||
|
uint8_t m2; /* 'G' */
|
||
|
uint32_t start_time;
|
||
|
uint32_t dts;
|
||
|
uint8_t packet_type; /* 0x16 = pcs_start/end, 0x17 = wds, 0x14 = palette, 0x15 = ods_first, 0x80 = null */
|
||
|
uint16_t packet_len;
|
||
|
} __attribute ((packed)) sup_header_t;
|
||
|
|
||
|
void conv_sup_header (sup_header_t *h)
|
||
|
{
|
||
|
h->start_time = SWAP32(h->start_time);
|
||
|
h->dts = SWAP32(h->dts);
|
||
|
h->packet_len = SWAP16(h->packet_len);
|
||
|
}
|
||
|
|
||
|
void print_ts (uint32_t ts)
|
||
|
{
|
||
|
printf("%.7fs (%u/90000s)", ((double)ts) / 90000, ts);
|
||
|
}
|
||
|
|
||
|
typedef struct sup_palette_s
|
||
|
{
|
||
|
uint16_t palette; /* FIXME */
|
||
|
} __attribute ((packed)) sup_palette_t;
|
||
|
|
||
|
void conv_sup_palette (sup_palette_t *p)
|
||
|
{
|
||
|
p->palette = SWAP16(p->palette);
|
||
|
}
|
||
|
|
||
|
void read_palette (parser_state_t *ps, sup_header_t h)
|
||
|
{
|
||
|
sup_palette_t p;
|
||
|
char *palette = malloc(5*256);
|
||
|
int i, p_size;
|
||
|
|
||
|
if (h.packet_len < 2+5)
|
||
|
die(ps->fh, sizeof(h), "Undersized palette packet.");
|
||
|
if (h.packet_len > 2+5*256)
|
||
|
die(ps->fh, sizeof(h), "Oversized palette packet.");
|
||
|
|
||
|
safe_read(&p, sizeof(p), ps->fh, "palette marker");
|
||
|
conv_sup_palette(&p);
|
||
|
|
||
|
printf("Palette\n");
|
||
|
printf("\tpalette = %u\n", p.palette);
|
||
|
(ps->palettes)++;
|
||
|
|
||
|
p_size = h.packet_len - 2;
|
||
|
safe_read(palette, p_size, ps->fh, "palette");
|
||
|
for (i = 0; i < p_size; i++)
|
||
|
if (palette[i] != ps->last_pal[i])
|
||
|
{
|
||
|
(ps->diff_palettes)++;
|
||
|
clear_palette(ps);
|
||
|
memcpy(ps->last_pal, palette, p_size);
|
||
|
break;
|
||
|
}
|
||
|
free(palette);
|
||
|
}
|
||
|
|
||
|
typedef struct sup_ods_next_s
|
||
|
{
|
||
|
uint16_t picture;
|
||
|
uint8_t m; /* 0 */
|
||
|
uint8_t last; /* 0 if not, if 64 yes */
|
||
|
} __attribute ((packed)) sup_ods_next_t;
|
||
|
|
||
|
void conv_sup_ods_next (sup_ods_next_t *odsn)
|
||
|
{
|
||
|
odsn->picture = SWAP16(odsn->picture);
|
||
|
}
|
||
|
|
||
|
void read_odsn (parser_state_t *ps, sup_header_t h)
|
||
|
{
|
||
|
sup_ods_next_t odsn;
|
||
|
|
||
|
if (h.packet_len < sizeof(odsn))
|
||
|
die(ps->fh, sizeof(h), "Undersized ODSN packet.");
|
||
|
|
||
|
safe_read(&odsn, sizeof(odsn), ps->fh, "ODSN structure");
|
||
|
conv_sup_ods_next(&odsn);
|
||
|
if (odsn.m != 0 || (odsn.last != 0 && odsn.last != 64))
|
||
|
die(ps->fh, sizeof(odsn), "Invalid ODSN magic.");
|
||
|
printf("ODS next\n");
|
||
|
printf("\tpicture = %u\n", odsn.picture);
|
||
|
printf("\tlast = %s (%u)\n", odsn.last ? "yes" : "no", odsn.last);
|
||
|
|
||
|
safe_seek(ps->fh, h.packet_len - sizeof(odsn), "ODSN data");
|
||
|
}
|
||
|
|
||
|
typedef struct sup_ods_first_s
|
||
|
{
|
||
|
uint16_t picture;
|
||
|
uint8_t palette; /* FIXME */
|
||
|
uint32_t magic_len; /* (single_packet ? 0xc0000000 : 0x80000000) | (length + 4) */
|
||
|
uint16_t width;
|
||
|
uint16_t height;
|
||
|
} __attribute ((packed)) sup_ods_first_t;
|
||
|
|
||
|
void conv_sup_ods_first (sup_ods_first_t *odsf)
|
||
|
{
|
||
|
odsf->picture = SWAP16(odsf->picture);
|
||
|
odsf->magic_len = SWAP32(odsf->magic_len);
|
||
|
odsf->width = SWAP16(odsf->width);
|
||
|
odsf->height = SWAP16(odsf->height);
|
||
|
}
|
||
|
|
||
|
void read_odsf (parser_state_t *ps, sup_header_t h)
|
||
|
{
|
||
|
sup_ods_first_t odsf;
|
||
|
|
||
|
if (h.packet_len < sizeof(odsf))
|
||
|
die(ps->fh, sizeof(h), "Undersized ODSF packet.");
|
||
|
|
||
|
safe_read(&odsf, sizeof(odsf), ps->fh, "ODSF structure");
|
||
|
conv_sup_ods_first(&odsf);
|
||
|
|
||
|
if ((odsf.magic_len & 0x80000000) && (odsf.magic_len & 0x40000000))
|
||
|
{
|
||
|
printf("ODS first\n\todsf type = single\n");
|
||
|
}
|
||
|
else if (odsf.magic_len & 0x80000000)
|
||
|
{
|
||
|
printf("ODS first\n\todsf type = multi\n");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
fseek(ps->fh, -sizeof(odsf), SEEK_CUR);
|
||
|
read_odsn(ps, h);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ps->total_object_sizes += odsf.width * odsf.height;
|
||
|
(ps->images)++;
|
||
|
printf("\tpicture = %u\n", odsf.picture);
|
||
|
printf("\tpalette = %u\n", odsf.palette);
|
||
|
printf("\tlength = %u (incl. + 4)\n", odsf.magic_len & 0x3fffffff);
|
||
|
printf("\twidth = %u\n", odsf.width);
|
||
|
printf("\theight = %u\n", odsf.height);
|
||
|
|
||
|
safe_seek(ps->fh, h.packet_len - sizeof(odsf), "OSDF data");
|
||
|
}
|
||
|
|
||
|
typedef struct sup_wds_s
|
||
|
{
|
||
|
uint8_t objects; /* 1 or 2 */
|
||
|
} __attribute ((packed)) sup_wds_t;
|
||
|
|
||
|
void conv_sup_wds (sup_wds_t *wds)
|
||
|
{
|
||
|
/* Do nothing. */
|
||
|
}
|
||
|
|
||
|
typedef struct sup_wds_obj_s
|
||
|
{
|
||
|
uint8_t object; /* 0 or 1 */
|
||
|
uint16_t x_off;
|
||
|
uint16_t y_off;
|
||
|
uint16_t width;
|
||
|
uint16_t height;
|
||
|
} __attribute ((packed)) sup_wds_obj_t;
|
||
|
|
||
|
void conv_sup_wds_obj (sup_wds_obj_t *wdso)
|
||
|
{
|
||
|
wdso->x_off = SWAP16(wdso->x_off);
|
||
|
wdso->y_off = SWAP16(wdso->y_off);
|
||
|
wdso->width = SWAP16(wdso->width);
|
||
|
wdso->height = SWAP16(wdso->height);
|
||
|
}
|
||
|
|
||
|
void read_wds (parser_state_t *ps, sup_header_t h)
|
||
|
{
|
||
|
sup_wds_t wds;
|
||
|
sup_wds_obj_t wdso;
|
||
|
int i;
|
||
|
|
||
|
if (h.packet_len != sizeof(wds) + sizeof(wdso) && h.packet_len != sizeof(wds) + 2 * sizeof(wdso))
|
||
|
die(ps->fh, sizeof(h), "Bad size for WDS packet.");
|
||
|
|
||
|
safe_read(&wds, sizeof(wds), ps->fh, "WDS structure");
|
||
|
conv_sup_wds(&wds);
|
||
|
if (wds.objects != 1 && wds.objects != 2)
|
||
|
die(ps->fh, sizeof(wds), "Invalid number of WDS objects.");
|
||
|
|
||
|
printf("WDS\n");
|
||
|
printf("\tobjects = %u\n", wds.objects);
|
||
|
|
||
|
for (i = 0; i < wds.objects; i++)
|
||
|
{
|
||
|
safe_read(&wdso, sizeof(wdso), ps->fh, "WDS object structure");
|
||
|
conv_sup_wds_obj(&wdso);
|
||
|
|
||
|
if (wdso.object != 0 && wdso.object != 1)
|
||
|
die(ps->fh, sizeof(wdso), "Invalid object id in WDS object.");
|
||
|
|
||
|
printf("\tObject %u\n", wdso.object + 1);
|
||
|
printf("\t\tx offset = %u\n", wdso.x_off);
|
||
|
printf("\t\ty offset = %u\n", wdso.y_off);
|
||
|
printf("\t\twidth = %u\n", wdso.width);
|
||
|
printf("\t\theight = %u\n", wdso.height);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
typedef struct sup_pcs_start_s
|
||
|
{
|
||
|
uint16_t width;
|
||
|
uint16_t height; /* height - 2 * Core.getCropOfsY */
|
||
|
uint8_t fps_id; /* getFpsId() */
|
||
|
uint16_t comp_num;
|
||
|
uint8_t follower; /* 0x80 if first or single, 0x40 if follows directly (end = start) or the frame after */
|
||
|
uint16_t m; /* 0 */
|
||
|
uint8_t objects; /* 1 */
|
||
|
} __attribute ((packed)) sup_pcs_start_t;
|
||
|
|
||
|
void conv_sup_pcs_start (sup_pcs_start_t *pcss)
|
||
|
{
|
||
|
pcss->width = SWAP16(pcss->width);
|
||
|
pcss->height = SWAP16(pcss->height);
|
||
|
pcss->comp_num = SWAP16(pcss->comp_num);
|
||
|
pcss->m = SWAP16(pcss->m);
|
||
|
}
|
||
|
|
||
|
typedef struct sup_pcs_start_obj_s
|
||
|
{
|
||
|
uint16_t picture;
|
||
|
uint8_t window;
|
||
|
uint8_t forced; /* forced ? 64 : 0 */
|
||
|
uint16_t x_off;
|
||
|
uint16_t y_off;
|
||
|
} __attribute ((packed)) sup_pcs_start_obj_t;
|
||
|
|
||
|
void conv_sup_pcs_start_obj (sup_pcs_start_obj_t *pcsso)
|
||
|
{
|
||
|
pcsso->picture = SWAP16(pcsso->picture);
|
||
|
pcsso->x_off = SWAP16(pcsso->x_off);
|
||
|
pcsso->y_off = SWAP16(pcsso->y_off);
|
||
|
}
|
||
|
|
||
|
void read_pcs_start (parser_state_t *ps, sup_header_t h)
|
||
|
{
|
||
|
sup_pcs_start_t pcss;
|
||
|
sup_pcs_start_obj_t pcsso;
|
||
|
int fps_num, fps_den, i;
|
||
|
|
||
|
safe_read(&pcss, sizeof(pcss), ps->fh, "PCSS structure");
|
||
|
conv_sup_pcs_start(&pcss);
|
||
|
|
||
|
if (pcss.m != 0)
|
||
|
die(ps->fh, sizeof(pcss), "Invalid PCSS magic.");
|
||
|
|
||
|
printf("PCS start\n");
|
||
|
|
||
|
printf("\tframe width = %u\n", pcss.width);
|
||
|
printf("\tframe height = %u\n", pcss.height);
|
||
|
if (!get_fps(pcss.fps_id, &fps_num, &fps_den))
|
||
|
die(ps->fh, sizeof(pcss), "Invalid FPS ID in PCSS.");
|
||
|
printf("\tfps id = %u (%u/%u)\n", pcss.fps_id, fps_num, fps_den);
|
||
|
printf("\tcomposition = %u\n", pcss.comp_num);
|
||
|
printf("\tfollower = 0x%02X (%s)\n", pcss.follower, pcss.follower == 0x80 ? "no" : "within 2f");
|
||
|
printf("\tobjects = %u\n", pcss.objects);
|
||
|
|
||
|
if (pcss.objects > 2)
|
||
|
die(ps->fh, sizeof(pcss), "Invalid number of objects (must be 1 or 2).");
|
||
|
|
||
|
for (i = 0; i < pcss.objects; i++)
|
||
|
{
|
||
|
safe_read(&pcsso, sizeof(pcsso), ps->fh, "PCSSO structure");
|
||
|
conv_sup_pcs_start_obj(&pcsso);
|
||
|
|
||
|
if (pcsso.window != 0 && pcsso.window != 1)
|
||
|
die(ps->fh, sizeof(pcsso), "Invalid window id in PCSS object.");
|
||
|
printf("\tObject %u\n", i + 1);
|
||
|
if (pcsso.forced && (pcsso.forced != 64))
|
||
|
die(ps->fh, sizeof(pcsso), "Invalid forced flag in PCSS object.");
|
||
|
printf("\t\tpicture = %u\n", pcsso.picture);
|
||
|
printf("\t\twindow = %u\n", pcsso.window);
|
||
|
printf("\t\tforced = %u\n", pcsso.forced);
|
||
|
printf("\t\tx offset = %u\n", pcsso.x_off);
|
||
|
printf("\t\ty offset = %u\n", pcsso.y_off);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
typedef struct sup_pcs_end_s
|
||
|
{
|
||
|
uint16_t width;
|
||
|
uint16_t height;
|
||
|
uint8_t fps_id;
|
||
|
uint16_t comp_num;
|
||
|
uint32_t m;
|
||
|
} __attribute ((packed)) sup_pcs_end_t;
|
||
|
|
||
|
void conv_sup_pcs_end (sup_pcs_end_t *pcse)
|
||
|
{
|
||
|
pcse->width = SWAP16(pcse->width);
|
||
|
pcse->height = SWAP16(pcse->height);
|
||
|
pcse->comp_num = SWAP16(pcse->comp_num);
|
||
|
pcse->m = SWAP32(pcse->m);
|
||
|
}
|
||
|
|
||
|
void read_pcs_end (parser_state_t *ps, sup_header_t h)
|
||
|
{
|
||
|
sup_pcs_end_t pcse;
|
||
|
int fps_num, fps_den;
|
||
|
|
||
|
safe_read(&pcse, sizeof(pcse), ps->fh, "PCSE structure");
|
||
|
conv_sup_pcs_end(&pcse);
|
||
|
|
||
|
if (pcse.m != 0)
|
||
|
die(ps->fh, sizeof(pcse), "Invalid PCSE magic.");
|
||
|
|
||
|
printf("PCS end\n");
|
||
|
|
||
|
printf("\tsubtitle width = %u\n", pcse.width);
|
||
|
printf("\tsubtitle height = %u\n", pcse.height);
|
||
|
if (!get_fps(pcse.fps_id, &fps_num, &fps_den))
|
||
|
die(ps->fh, sizeof(pcse), "Invalid FPS ID in PCSE.");
|
||
|
printf("\tfps id = %u (%u/%u)\n", pcse.fps_id, fps_num, fps_den);
|
||
|
printf("\tcomposition = %u\n", pcse.comp_num);
|
||
|
printf("\tStats\n\t\ttot_ob_size = %u\n\t\timages = %u\n\t\tpalettes = %u\n\t\tdiff_palettes = %u\n", ps->total_object_sizes, ps->images, ps->palettes, ps->diff_palettes);
|
||
|
clear_stats(ps);
|
||
|
clear_palette(ps);
|
||
|
}
|
||
|
|
||
|
void read_sup (parser_state_t *ps)
|
||
|
{
|
||
|
sup_header_t h;
|
||
|
|
||
|
safe_read(&h, sizeof(h), ps->fh, "PG header");
|
||
|
conv_sup_header(&h);
|
||
|
|
||
|
if (h.m1 != 80 || h.m2 != 71)
|
||
|
die(ps->fh, sizeof(h), "Invalid PG header.");
|
||
|
|
||
|
printf("Packet at 0x%08x:\nstart_time = ", (unsigned int)(ftell(ps->fh) - sizeof(h)));
|
||
|
print_ts(h.start_time);
|
||
|
printf("\ndts = ");
|
||
|
print_ts(h.dts);
|
||
|
printf("\ntype = 0x%02X\n", h.packet_type);
|
||
|
printf("length = %u\n", h.packet_len);
|
||
|
switch (h.packet_type)
|
||
|
{
|
||
|
case 0x14:
|
||
|
read_palette(ps, h);
|
||
|
break;
|
||
|
case 0x15:
|
||
|
read_odsf(ps, h);
|
||
|
break;
|
||
|
case 0x16:
|
||
|
switch (h.packet_len)
|
||
|
{
|
||
|
case 11:
|
||
|
read_pcs_end(ps, h);
|
||
|
break;
|
||
|
case 19:
|
||
|
case 27:
|
||
|
read_pcs_start(ps, h);
|
||
|
break;
|
||
|
default:
|
||
|
die(ps->fh, sizeof(h), "Invalid PCS size.");
|
||
|
}
|
||
|
break;
|
||
|
case 0x17:
|
||
|
read_wds(ps, h);
|
||
|
break;
|
||
|
case 0x80:
|
||
|
if (h.packet_len == 0)
|
||
|
printf("Marker\n");
|
||
|
else
|
||
|
die(ps->fh, sizeof(h), "Marker with payload.");
|
||
|
break;
|
||
|
default:
|
||
|
die(ps->fh, sizeof(h), "Unknown packet type.");
|
||
|
}
|
||
|
|
||
|
printf("\n");
|
||
|
}
|
||
|
|
||
|
int main (int argc, char *argv[])
|
||
|
{
|
||
|
parser_state_t *ps;
|
||
|
size_t last;
|
||
|
|
||
|
if (argc != 2)
|
||
|
{
|
||
|
printf("Usage: pgsparse SUPFILE\n");
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
ps = new_sup_parser(argv[1]);
|
||
|
|
||
|
fseek(ps->fh, 0, SEEK_END);
|
||
|
last = ftell(ps->fh);
|
||
|
fseek(ps->fh, 0, SEEK_SET);
|
||
|
|
||
|
while (!feof(ps->fh) && ftell(ps->fh) < last)
|
||
|
read_sup(ps);
|
||
|
|
||
|
fclose(ps->fh);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|