run: Don't allow chroot()

If we don't allow pivot_root() then there seems no reason why we should
allow chroot().

Partially fixes GHSA-67h7-w3jq-vh4q.

Signed-off-by: Simon McVittie <smcv@collabora.com>

setup_seccomp (FlatpakBwrap   *bwrap,
               const char     *arch,
               gulong          allowed_personality,
               FlatpakRunFlags run_flags,
               GError        **error)
{
  gboolean multiarch = (run_flags & FLATPAK_RUN_FLAG_MULTIARCH) != 0;
  gboolean devel = (run_flags & FLATPAK_RUN_FLAG_DEVEL) != 0;

  __attribute__((cleanup (cleanup_seccomp))) scmp_filter_ctx seccomp = NULL;

  /**** BEGIN NOTE ON CODE SHARING
   *
   * There are today a number of different Linux container
   * implementations.  That will likely continue for long into the
   * future.  But we can still try to share code, and it's important
   * to do so because it affects what library and application writers
   * can do, and we should support code portability between different
   * container tools.
   *
   * This syscall blocklist is copied from linux-user-chroot, which was in turn
   * clearly influenced by the Sandstorm.io blocklist.
   *
   * If you make any changes here, I suggest sending the changes along
   * to other sandbox maintainers.  Using the libseccomp list is also
   * an appropriate venue:
   * https://groups.google.com/forum/#!forum/libseccomp
   *
   * A non-exhaustive list of links to container tooling that might
   * want to share this blocklist:
   *
   *  https://github.com/sandstorm-io/sandstorm
   *    in src/sandstorm/supervisor.c++
   *  https://github.com/flatpak/flatpak.git
   *    in common/flatpak-run.c
   *  https://git.gnome.org/browse/linux-user-chroot
   *    in src/setup-seccomp.c
   *
   * Other useful resources:
   * https://github.com/systemd/systemd/blob/HEAD/src/shared/seccomp-util.c
   * https://github.com/moby/moby/blob/HEAD/profiles/seccomp/default.json
   *
   **** END NOTE ON CODE SHARING
   */
  struct
  {
    int                  scall;
    int                  errnum;
    struct scmp_arg_cmp *arg;
  } syscall_blocklist[] = {
    /* Block dmesg */
    {SCMP_SYS (syslog), EPERM},
    /* Useless old syscall */
    {SCMP_SYS (uselib), EPERM},
    /* Don't allow disabling accounting */
    {SCMP_SYS (acct), EPERM},
    /* 16-bit code is unnecessary in the sandbox, and modify_ldt is a
       historic source of interesting information leaks. */
    {SCMP_SYS (modify_ldt), EPERM},
    /* Don't allow reading current quota use */
    {SCMP_SYS (quotactl), EPERM},

    /* Don't allow access to the kernel keyring */
    {SCMP_SYS (add_key), EPERM},
    {SCMP_SYS (keyctl), EPERM},
    {SCMP_SYS (request_key), EPERM},

    /* Scary VM/NUMA ops */
    {SCMP_SYS (move_pages), EPERM},
    {SCMP_SYS (mbind), EPERM},
    {SCMP_SYS (get_mempolicy), EPERM},
    {SCMP_SYS (set_mempolicy), EPERM},
    {SCMP_SYS (migrate_pages), EPERM},

    /* Don't allow subnamespace setups: */
    {SCMP_SYS (unshare), EPERM},
    {SCMP_SYS (setns), EPERM},
    {SCMP_SYS (mount), EPERM},
    {SCMP_SYS (umount), EPERM},
    {SCMP_SYS (umount2), EPERM},
    {SCMP_SYS (pivot_root), EPERM},
    {SCMP_SYS (chroot), EPERM},
#if defined(__s390__) || defined(__s390x__) || defined(__CRIS__)
    /* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
     * and flags arguments are reversed so the flags come second */
    {SCMP_SYS (clone), EPERM, &SCMP_A1 (SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
#else
    /* Normally the flags come first */
    {SCMP_SYS (clone), EPERM, &SCMP_A0 (SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
#endif

    /* Don't allow faking input to the controlling tty (CVE-2017-5226) */
    {SCMP_SYS (ioctl), EPERM, &SCMP_A1 (SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int) TIOCSTI)},

    /* seccomp can't look into clone3()'s struct clone_args to check whether
     * the flags are OK, so we have no choice but to block clone3().
     * Return ENOSYS so user-space will fall back to clone().
     * (GHSA-67h7-w3jq-vh4q; see also https://github.com/moby/moby/commit/9f6b562d) */
    {SCMP_SYS (clone3), ENOSYS},

    /* New mount manipulation APIs can also change our VFS. There's no
     * legitimate reason to do these in the sandbox, so block all of them
     * rather than thinking about which ones might be dangerous.
     * (GHSA-67h7-w3jq-vh4q) */
    {SCMP_SYS (open_tree), ENOSYS},
    {SCMP_SYS (move_mount), ENOSYS},
    {SCMP_SYS (fsopen), ENOSYS},
    {SCMP_SYS (fsconfig), ENOSYS},
    {SCMP_SYS (fsmount), ENOSYS},
    {SCMP_SYS (fspick), ENOSYS},
    {SCMP_SYS (mount_setattr), ENOSYS},
  };

  struct
  {
    int                  scall;
    int                  errnum;
    struct scmp_arg_cmp *arg;
  } syscall_nondevel_blocklist[] = {
    /* Profiling operations; we expect these to be done by tools from outside
     * the sandbox.  In particular perf has been the source of many CVEs.
     */
    {SCMP_SYS (perf_event_open), EPERM},
    /* Don't allow you to switch to bsd emulation or whatnot */
    {SCMP_SYS (personality), EPERM, &SCMP_A0 (SCMP_CMP_NE, allowed_personality)},
    {SCMP_SYS (ptrace), EPERM}
  };
  /* Blocklist all but unix, inet, inet6 and netlink */
  struct
  {
    int             family;
    FlatpakRunFlags flags_mask;
  } socket_family_allowlist[] = {
    /* NOTE: Keep in numerical order */
    { AF_UNSPEC, 0 },
    { AF_LOCAL, 0 },
    { AF_INET, 0 },
    { AF_INET6, 0 },
    { AF_NETLINK, 0 },
    { AF_CAN, FLATPAK_RUN_FLAG_CANBUS },
    { AF_BLUETOOTH, FLATPAK_RUN_FLAG_BLUETOOTH },
  };
  int last_allowed_family;
  int i, r;
  g_auto(GLnxTmpfile) seccomp_tmpf  = { 0, };

  seccomp = seccomp_init (SCMP_ACT_ALLOW);
  if (!seccomp)
    return flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Initialize seccomp failed"));

  if (arch != NULL)
    {
      uint32_t arch_id = 0;
      const uint32_t *extra_arches = NULL;

      if (strcmp (arch, "i386") == 0)
        {
          arch_id = SCMP_ARCH_X86;
        }
      else if (strcmp (arch, "x86_64") == 0)
        {
          arch_id = SCMP_ARCH_X86_64;
          extra_arches = seccomp_x86_64_extra_arches;
        }
      else if (strcmp (arch, "arm") == 0)
        {
          arch_id = SCMP_ARCH_ARM;
        }
#ifdef SCMP_ARCH_AARCH64
      else if (strcmp (arch, "aarch64") == 0)
        {
          arch_id = SCMP_ARCH_AARCH64;
          extra_arches = seccomp_aarch64_extra_arches;
        }
#endif

      /* We only really need to handle arches on multiarch systems.
       * If only one arch is supported the default is fine */
      if (arch_id != 0)
        {
          /* This *adds* the target arch, instead of replacing the
             native one. This is not ideal, because we'd like to only
             allow the target arch, but we can't really disallow the
             native arch at this point, because then bubblewrap
             couldn't continue running. */
          r = seccomp_arch_add (seccomp, arch_id);
          if (r < 0 && r != -EEXIST)
            return flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Failed to add architecture to seccomp filter"));

          if (multiarch && extra_arches != NULL)
            {
              for (i = 0; extra_arches[i] != 0; i++)
                {
                  r = seccomp_arch_add (seccomp, extra_arches[i]);
                  if (r < 0 && r != -EEXIST)
                    return flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Failed to add multiarch architecture to seccomp filter"));
                }
            }
        }
    }

  /* TODO: Should we filter the kernel keyring syscalls in some way?
   * We do want them to be used by desktop apps, but they could also perhaps
   * leak system stuff or secrets from other apps.
   */

  for (i = 0; i < G_N_ELEMENTS (syscall_blocklist); i++)
    {
      int scall = syscall_blocklist[i].scall;
      int errnum = syscall_blocklist[i].errnum;

      g_return_val_if_fail (errnum == EPERM || errnum == ENOSYS, FALSE);

      if (syscall_blocklist[i].arg)
        r = seccomp_rule_add (seccomp, SCMP_ACT_ERRNO (errnum), scall, 1, *syscall_blocklist[i].arg);
      else
        r = seccomp_rule_add (seccomp, SCMP_ACT_ERRNO (errnum), scall, 0);
      if (r < 0 && r == -EFAULT /* unknown syscall */)
        return flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Failed to block syscall %d"), scall);
    }

  if (!devel)
    {
      for (i = 0; i < G_N_ELEMENTS (syscall_nondevel_blocklist); i++)
        {
          int scall = syscall_nondevel_blocklist[i].scall;
          int errnum = syscall_nondevel_blocklist[i].errnum;

          g_return_val_if_fail (errnum == EPERM || errnum == ENOSYS, FALSE);

          if (syscall_nondevel_blocklist[i].arg)
            r = seccomp_rule_add (seccomp, SCMP_ACT_ERRNO (errnum), scall, 1, *syscall_nondevel_blocklist[i].arg);
          else
            r = seccomp_rule_add (seccomp, SCMP_ACT_ERRNO (errnum), scall, 0);

          if (r < 0 && r == -EFAULT /* unknown syscall */)
            return flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Failed to block syscall %d"), scall);
        }
    }

  /* Socket filtering doesn't work on e.g. i386, so ignore failures here
   * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
   * something else: https://github.com/seccomp/libseccomp/issues/8 */
  last_allowed_family = -1;
  for (i = 0; i < G_N_ELEMENTS (socket_family_allowlist); i++)
    {
      int family = socket_family_allowlist[i].family;
      int disallowed;

      if (socket_family_allowlist[i].flags_mask != 0 &&
          (socket_family_allowlist[i].flags_mask & run_flags) != socket_family_allowlist[i].flags_mask)
        continue;

      for (disallowed = last_allowed_family + 1; disallowed < family; disallowed++)
        {
          /* Blocklist the in-between valid families */
          seccomp_rule_add_exact (seccomp, SCMP_ACT_ERRNO (EAFNOSUPPORT), SCMP_SYS (socket), 1, SCMP_A0 (SCMP_CMP_EQ, disallowed));
        }
      last_allowed_family = family;
    }
  /* Blocklist the rest */
  seccomp_rule_add_exact (seccomp, SCMP_ACT_ERRNO (EAFNOSUPPORT), SCMP_SYS (socket), 1, SCMP_A0 (SCMP_CMP_GE, last_allowed_family + 1));

  if (!glnx_open_anonymous_tmpfile_full (O_RDWR | O_CLOEXEC, "/tmp", &seccomp_tmpf, error))
    return FALSE;

  if (seccomp_export_bpf (seccomp, seccomp_tmpf.fd) != 0)
    return flatpak_fail_error (error, FLATPAK_ERROR_SETUP_FAILED, _("Failed to export bpf"));

  lseek (seccomp_tmpf.fd, 0, SEEK_SET);

  flatpak_bwrap_add_args_data_fd (bwrap,
                                  "--seccomp", glnx_steal_fd (&seccomp_tmpf.fd), NULL);

  return TRUE;
}

Flatpak is a system for building, distributing, and running sandboxed desktop applications on Linux. In versions prior to 1.10.4 and 1.12.0, Flatpak apps with direct access to AF_UNIX sockets such as those used by Wayland, Pipewire or pipewire-pulse can trick portals and other host-OS services into treating the Flatpak app as though it was an ordinary, non-sandboxed host-OS process. They can do this by manipulating the VFS using recent mount-related syscalls that are not blocked by Flatpak's denylist seccomp filter, in order to substitute a crafted `/.flatpak-info` or make that file disappear entirely. Flatpak apps that act as clients for AF_UNIX sockets such as those used by Wayland, Pipewire or pipewire-pulse can escalate the privileges that the corresponding services will believe the Flatpak app has. Note that protocols that operate entirely over the D-Bus session bus (user bus), system bus or accessibility bus are not affected by this. This is due to the use of a proxy process `xdg-dbus-proxy`, whose VFS cannot be manipulated by the Flatpak app, when interacting with these buses. Patches exist for versions 1.10.4 and 1.12.0, and as of time of publication, a patch for version 1.8.2 is being planned. There are no workarounds aside from upgrading to a patched version.


Improve handling of Gopher responses (#1022)

GopherStateData::~GopherStateData()
{
    if (entry)
        entry->unlock("gopherState");

    if (buf)
        memFree(buf, MEM_4K_BUF);
}

In Squid 3.x through 3.5.28, 4.x through 4.17, and 5.x before 5.6, due to improper buffer management, a Denial of Service can occur when processing long Gopher server responses.


gopherEndHTML(GopherStateData * gopherState)
{
    StoreEntry *e = gopherState->entry;

    if (!gopherState->HTML_header_added) {
        gopherHTMLHeader(e, "Server Return Nothing", NULL);
        storeAppendPrintf(e, "
The Gopher query resulted in a blank response
");
    } else if (gopherState->HTML_pre) {
        storeAppendPrintf(e, "
\n");
    }

    gopherHTMLFooter(e);
}


gopherReadReply(const Comm::ConnectionPointer &conn, char *buf, size_t len, Comm::Flag flag, int xerrno, void *data)
{
    GopherStateData *gopherState = (GopherStateData *)data;
    StoreEntry *entry = gopherState->entry;
    int clen;
    int bin;
    size_t read_sz = BUFSIZ;
#if USE_DELAY_POOLS
    DelayId delayId = entry->mem_obj->mostBytesAllowed();
#endif

    /* Bail out early on Comm::ERR_CLOSING - close handlers will tidy up for us */

    if (flag == Comm::ERR_CLOSING) {
        return;
    }

    assert(buf == gopherState->replybuf);

    // XXX: Should update delayId, statCounter, etc. before bailing
    if (!entry->isAccepting()) {
        debugs(10, 3, "terminating due to bad " << *entry);
        // TODO: Do not abuse connection for triggering cleanup.
        gopherState->serverConn->close();
        return;
    }

#if USE_DELAY_POOLS
    read_sz = delayId.bytesWanted(1, read_sz);
#endif

    /* leave one space for \0 in gopherToHTML */

    if (flag == Comm::OK && len > 0) {
#if USE_DELAY_POOLS
        delayId.bytesIn(len);
#endif

        statCounter.server.all.kbytes_in += len;
        statCounter.server.other.kbytes_in += len;
    }

    debugs(10, 5, conn << " read len=" << len);

    if (flag == Comm::OK && len > 0) {
        AsyncCall::Pointer nil;
        commSetConnTimeout(conn, Config.Timeout.read, nil);
        ++IOStats.Gopher.reads;

        for (clen = len - 1, bin = 0; clen; ++bin)
            clen >>= 1;

        ++IOStats.Gopher.read_hist[bin];

        HttpRequest *req = gopherState->fwd->request;
        if (req->hier.bodyBytesRead < 0) {
            req->hier.bodyBytesRead = 0;
            // first bytes read, update Reply flags:
            gopherState->reply_->sources |= Http::Message::srcGopher;
        }

        req->hier.bodyBytesRead += len;
    }

    if (flag != Comm::OK) {
        debugs(50, DBG_IMPORTANT, "ERROR: " << MYNAME << "reading: " << xstrerr(xerrno));

        if (ignoreErrno(xerrno)) {
            AsyncCall::Pointer call = commCbCall(5,4, "gopherReadReply",
                                                 CommIoCbPtrFun(gopherReadReply, gopherState));
            comm_read(conn, buf, read_sz, call);
        } else {
            const auto err = new ErrorState(ERR_READ_ERROR, Http::scInternalServerError, gopherState->fwd->request, gopherState->fwd->al);
            err->xerrno = xerrno;
            gopherState->fwd->fail(err);
            gopherState->serverConn->close();
        }
    } else if (len == 0 && entry->isEmpty()) {
        gopherState->fwd->fail(new ErrorState(ERR_ZERO_SIZE_OBJECT, Http::scServiceUnavailable, gopherState->fwd->request, gopherState->fwd->al));
        gopherState->serverConn->close();
    } else if (len == 0) {
        /* Connection closed; retrieval done. */
        /* flush the rest of data in temp buf if there is one. */

        if (gopherState->conversion != GopherStateData::NORMAL)
            gopherEndHTML(gopherState);

        entry->timestampsSet();
        entry->flush();

        if (!gopherState->len && !gopherState->overflowed)
            gopherState->fwd->markStoredReplyAsWhole("gopher EOF after receiving/storing some bytes");

        gopherState->fwd->complete();
        gopherState->serverConn->close();
    } else {
        if (gopherState->conversion != GopherStateData::NORMAL) {
            gopherToHTML(gopherState, buf, len);
        } else {
            entry->append(buf, len);
        }
        AsyncCall::Pointer call = commCbCall(5,4, "gopherReadReply",
                                             CommIoCbPtrFun(gopherReadReply, gopherState));
        comm_read(conn, buf, read_sz, call);
    }
}


gopherToHTML(GopherStateData * gopherState, char *inbuf, int len)
{
    char *pos = inbuf;
    char *lpos = NULL;
    char *tline = NULL;
    LOCAL_ARRAY(char, line, TEMP_BUF_SIZE);
    char *name = NULL;
    char *selector = NULL;
    char *host = NULL;
    char *port = NULL;
    char *escaped_selector = NULL;
    const char *icon_url = NULL;
    char gtype;
    StoreEntry *entry = NULL;

    memset(line, '\0', TEMP_BUF_SIZE);

    entry = gopherState->entry;

    if (gopherState->conversion == GopherStateData::HTML_INDEX_PAGE) {
        char *html_url = html_quote(entry->url());
        gopherHTMLHeader(entry, "Gopher Index %s", html_url);
        storeAppendPrintf(entry,
                          "
This is a searchable Gopher index. Use the search\n"
                          "function of your browser to enter search terms.\n"
                          "<ISINDEX>\n");
        gopherHTMLFooter(entry);
        /* now let start sending stuff to client */
        entry->flush();
        gopherState->HTML_header_added = 1;

        return;
    }

    if (gopherState->conversion == GopherStateData::HTML_CSO_PAGE) {
        char *html_url = html_quote(entry->url());
        gopherHTMLHeader(entry, "CSO Search of %s", html_url);
        storeAppendPrintf(entry,
                          "
A CSO database usually contains a phonebook or\n"
                          "directory.  Use the search function of your browser to enter\n"
                          "search terms.
<ISINDEX>\n");
        gopherHTMLFooter(entry);
        /* now let start sending stuff to client */
        entry->flush();
        gopherState->HTML_header_added = 1;

        return;
    }

    SBuf outbuf;

    if (!gopherState->HTML_header_added) {
        if (gopherState->conversion == GopherStateData::HTML_CSO_RESULT)
            gopherHTMLHeader(entry, "CSO Search Result", NULL);
        else
            gopherHTMLHeader(entry, "Gopher Menu", NULL);

        outbuf.append ("
");

        gopherState->HTML_header_added = 1;

        gopherState->HTML_pre = 1;
    }

    while (pos < inbuf + len) {
        int llen;
        int left = len - (pos - inbuf);
        lpos = (char *)memchr(pos, '\n', left);
        if (lpos) {
            ++lpos;             /* Next line is after \n */
            llen = lpos - pos;
        } else {
            llen = left;
        }
        if (gopherState->len + llen >= TEMP_BUF_SIZE) {
            debugs(10, DBG_IMPORTANT, "GopherHTML: Buffer overflow. Lost some data on URL: " << entry->url()  );
            llen = TEMP_BUF_SIZE - gopherState->len - 1;
            gopherState->overflowed = true; // may already be true
        }
        if (!lpos) {
            /* there is no complete line in inbuf */
            /* copy it to temp buffer */
            /* note: llen is adjusted above */
            memcpy(gopherState->buf + gopherState->len, pos, llen);
            gopherState->len += llen;
            break;
        }
        if (gopherState->len != 0) {
            /* there is something left from last tx. */
            memcpy(line, gopherState->buf, gopherState->len);
            memcpy(line + gopherState->len, pos, llen);
            llen += gopherState->len;
            gopherState->len = 0;
        } else {
            memcpy(line, pos, llen);
        }
        line[llen + 1] = '\0';
        /* move input to next line */
        pos = lpos;

        /* at this point. We should have one line in buffer to process */

        if (*line == '.') {
            /* skip it */
            memset(line, '\0', TEMP_BUF_SIZE);
            continue;
        }

        switch (gopherState->conversion) {

        case GopherStateData::HTML_INDEX_RESULT:

        case GopherStateData::HTML_DIR: {
            tline = line;
            gtype = *tline;
            ++tline;
            name = tline;
            selector = strchr(tline, TAB);

            if (selector) {
                *selector = '\0';
                ++selector;
                host = strchr(selector, TAB);

                if (host) {
                    *host = '\0';
                    ++host;
                    port = strchr(host, TAB);

                    if (port) {
                        char *junk;
                        port[0] = ':';
                        junk = strchr(host, TAB);

                        if (junk)
                            *junk++ = 0;    /* Chop port */
                        else {
                            junk = strchr(host, '\r');

                            if (junk)
                                *junk++ = 0;    /* Chop port */
                            else {
                                junk = strchr(host, '\n');

                                if (junk)
                                    *junk++ = 0;    /* Chop port */
                            }
                        }

                        if ((port[1] == '0') && (!port[2]))
                            port[0] = 0;    /* 0 means none */
                    }

                    /* escape a selector here */
                    escaped_selector = xstrdup(rfc1738_escape_part(selector));

                    switch (gtype) {

                    case GOPHER_DIRECTORY:
                        icon_url = mimeGetIconURL("internal-menu");
                        break;

                    case GOPHER_HTML:

                    case GOPHER_FILE:
                        icon_url = mimeGetIconURL("internal-text");
                        break;

                    case GOPHER_INDEX:

                    case GOPHER_CSO:
                        icon_url = mimeGetIconURL("internal-index");
                        break;

                    case GOPHER_IMAGE:

                    case GOPHER_GIF:

                    case GOPHER_PLUS_IMAGE:
                        icon_url = mimeGetIconURL("internal-image");
                        break;

                    case GOPHER_SOUND:

                    case GOPHER_PLUS_SOUND:
                        icon_url = mimeGetIconURL("internal-sound");
                        break;

                    case GOPHER_PLUS_MOVIE:
                        icon_url = mimeGetIconURL("internal-movie");
                        break;

                    case GOPHER_TELNET:

                    case GOPHER_3270:
                        icon_url = mimeGetIconURL("internal-telnet");
                        break;

                    case GOPHER_BIN:

                    case GOPHER_MACBINHEX:

                    case GOPHER_DOSBIN:

                    case GOPHER_UUENCODED:
                        icon_url = mimeGetIconURL("internal-binary");
                        break;

                    case GOPHER_INFO:
                        icon_url = NULL;
                        break;

                    case GOPHER_WWW:
                        icon_url = mimeGetIconURL("internal-link");
                        break;

                    default:
                        icon_url = mimeGetIconURL("internal-unknown");
                        break;
                    }

                    if ((gtype == GOPHER_TELNET) || (gtype == GOPHER_3270)) {
                        if (strlen(escaped_selector) != 0)
                            outbuf.appendf(" %s\n",
                                     icon_url, escaped_selector, rfc1738_escape_part(host),
                                     *port ? ":" : "", port, html_quote(name));
                        else
                            outbuf.appendf(" %s\n",
                                     icon_url, rfc1738_escape_part(host), *port ? ":" : "",
                                     port, html_quote(name));

                    } else if (gtype == GOPHER_INFO) {
                        outbuf.appendf("\t%s\n", html_quote(name));
                    } else {
                        if (strncmp(selector, "GET /", 5) == 0) {
                            /* WWW link */
                            outbuf.appendf(" %s\n",
                                     icon_url, host, rfc1738_escape_unescaped(selector + 5), html_quote(name));
                        } else if (gtype == GOPHER_WWW) {
                            outbuf.appendf(" %s\n",
                                     icon_url, rfc1738_escape_unescaped(selector), html_quote(name));
                        } else {
                            /* Standard link */
                            outbuf.appendf(" %s\n",
                                     icon_url, host, gtype, escaped_selector, html_quote(name));
                        }
                    }

                    safe_free(escaped_selector);
                } else {
                    memset(line, '\0', TEMP_BUF_SIZE);
                    continue;
                }
            } else {
                memset(line, '\0', TEMP_BUF_SIZE);
                continue;
            }

            break;
            }           /* HTML_DIR, HTML_INDEX_RESULT */

        case GopherStateData::HTML_CSO_RESULT: {
            if (line[0] == '-') {
                int code, recno;
                char *s_code, *s_recno, *result;

                s_code = strtok(line + 1, ":\n");
                s_recno = strtok(NULL, ":\n");
                result = strtok(NULL, "\n");

                if (!result)
                    break;

                code = atoi(s_code);

                recno = atoi(s_recno);

                if (code != 200)
                    break;

                if (gopherState->cso_recno != recno) {
                    outbuf.appendf("

Record# %d
%s
\n
", recno, html_quote(result));
                    gopherState->cso_recno = recno;
                } else {
                    outbuf.appendf("%s\n", html_quote(result));
                }

                break;
            } else {
                int code;
                char *s_code, *result;

                s_code = strtok(line, ":");
                result = strtok(NULL, "\n");

                if (!result)
                    break;

                code = atoi(s_code);

                switch (code) {

                case 200: {
                    /* OK */
                    /* Do nothing here */
                    break;
                }

                case 102:   /* Number of matches */

                case 501:   /* No Match */

                case 502: { /* Too Many Matches */
                    /* Print the message the server returns */
                    outbuf.appendf("

%s
\n
", html_quote(result));
                    break;
                }

                }
            }

            break;
            }           /* HTML_CSO_RESULT */
        default:
            break;      /* do nothing */

        }           /* switch */

    }               /* while loop */

    if (outbuf.length() > 0) {
        entry->append(outbuf.rawContent(), outbuf.length());
        /* now let start sending stuff to client */
        entry->flush();
    }

    return;
}


gopherHTMLHeader(StoreEntry * e, const char *title, const char *substring)
{
    storeAppendPrintf(e, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n");
    storeAppendPrintf(e, "<TITLE>");
    storeAppendPrintf(e, title, substring);
    storeAppendPrintf(e, "</TITLE>");
    storeAppendPrintf(e, "\n");
    storeAppendPrintf(e, "\n
");
    storeAppendPrintf(e, title, substring);
    storeAppendPrintf(e, "
\n");
}


    GopherStateData(FwdState *aFwd) :
        entry(aFwd->entry),
        conversion(NORMAL),
        HTML_header_added(0),
        HTML_pre(0),
        type_id(GOPHER_FILE /* '0' */),
        overflowed(false),
        cso_recno(0),
        len(0),
        buf(NULL),
        fwd(aFwd)
    {
        *request = 0;
        buf = (char *)memAllocate(MEM_4K_BUF);
        entry->lock("gopherState");
        *replybuf = 0;
    }


gopherStateFree(const CommCloseCbParams &params)
{
    GopherStateData *gopherState = (GopherStateData *)params.data;
    // Assume that FwdState is monitoring and calls noteClosure(). See XXX about
    // Connection sharing with FwdState in gopherStart().
    delete gopherState;
}


gopher_request_parse(const HttpRequest * req, char *type_id, char *request)
{
    ::Parser::Tokenizer tok(req->url.path());

    if (request)
        *request = 0;

    tok.skip('/'); // ignore failures? path could be ab-empty

    if (tok.atEnd()) {
        *type_id = GOPHER_DIRECTORY;
        return;
    }

    static const CharacterSet anyByte("UTF-8",0x00, 0xFF);

    SBuf typeId;
    (void)tok.prefix(typeId, anyByte, 1); // never fails since !atEnd()
    *type_id = typeId[0];

    if (request) {
        SBufToCstring(request, tok.remaining().substr(0, MAX_URL-1));
        /* convert %xx to char */
        rfc1738_unescape(request);
    }
}


gopherSendRequest(int, void *data)
{
    GopherStateData *gopherState = (GopherStateData *)data;
    MemBuf mb;
    mb.init();

    if (gopherState->type_id == GOPHER_CSO) {
        const char *t = strchr(gopherState->request, '?');

        if (t)
            ++t;        /* skip the ? */
        else
            t = "";

        mb.appendf("query %s\r\nquit", t);
    } else {
        if (gopherState->type_id == GOPHER_INDEX) {
            if (char *t = strchr(gopherState->request, '?'))
                *t = '\t';
        }
        mb.append(gopherState->request, strlen(gopherState->request));
    }
    mb.append("\r\n", 2);

    debugs(10, 5, gopherState->serverConn);
    AsyncCall::Pointer call = commCbCall(5,5, "gopherSendComplete",
                                         CommIoCbPtrFun(gopherSendComplete, gopherState));
    Comm::Write(gopherState->serverConn, &mb, call);

    if (!gopherState->entry->makePublic())
        gopherState->entry->makePrivate(true);
}


gopherStart(FwdState * fwd)
{
    GopherStateData *gopherState = new GopherStateData(fwd);

    debugs(10, 3, gopherState->entry->url());

    ++ statCounter.server.all.requests;

    ++ statCounter.server.other.requests;

    /* Parse url. */
    gopher_request_parse(fwd->request,
                         &gopherState->type_id, gopherState->request);

    comm_add_close_handler(fwd->serverConnection()->fd, gopherStateFree, gopherState);

    if (((gopherState->type_id == GOPHER_INDEX) || (gopherState->type_id == GOPHER_CSO))
            && (strchr(gopherState->request, '?') == NULL)) {
        /* Index URL without query word */
        /* We have to generate search page back to client. No need for connection */
        gopherMimeCreate(gopherState);

        if (gopherState->type_id == GOPHER_INDEX) {
            gopherState->conversion = GopherStateData::HTML_INDEX_PAGE;
        } else {
            if (gopherState->type_id == GOPHER_CSO) {
                gopherState->conversion = GopherStateData::HTML_CSO_PAGE;
            } else {
                gopherState->conversion = GopherStateData::HTML_INDEX_PAGE;
            }
        }

        gopherToHTML(gopherState, (char *) NULL, 0);
        fwd->markStoredReplyAsWhole("gopher instant internal request satisfaction");
        fwd->complete();
        return;
    }

    // XXX: Sharing open Connection with FwdState that has its own handlers/etc.
    gopherState->serverConn = fwd->serverConnection();
    gopherSendRequest(fwd->serverConnection()->fd, gopherState);
    AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "gopherTimeout",
                                     CommTimeoutCbPtrFun(gopherTimeout, gopherState));
    commSetConnTimeout(fwd->serverConnection(), Config.Timeout.read, timeoutCall);
}


gopherTimeout(const CommTimeoutCbParams &io)
{
    GopherStateData *gopherState = static_cast<GopherStateData *>(io.data);
    debugs(10, 4, io.conn << ": '" << gopherState->entry->url() << "'" );

    gopherState->fwd->fail(new ErrorState(ERR_READ_TIMEOUT, Http::scGatewayTimeout, gopherState->fwd->request, gopherState->fwd->al));

    if (Comm::IsConnOpen(io.conn))
        io.conn->close();
}


gopherHTMLFooter(StoreEntry * e)
{
    storeAppendPrintf(e, "
\n");
    storeAppendPrintf(e, "<ADDRESS>\n");
    storeAppendPrintf(e, "Generated %s by %s (%s)\n",
                      Time::FormatRfc1123(squid_curtime),
                      getMyHostname(),
                      visible_appname_string);
    storeAppendPrintf(e, "</ADDRESS>\n");
}


gopherCachable(const HttpRequest * req)
{
    int cachable = 1;
    char type_id;
    /* parse to see type */
    gopher_request_parse(req,
                         &type_id,
                         NULL);

    switch (type_id) {

    case GOPHER_INDEX:

    case GOPHER_CSO:

    case GOPHER_TELNET:

    case GOPHER_3270:
        cachable = 0;
        break;

    default:
        cachable = 1;
    }

    return cachable;
}


gopherSendComplete(const Comm::ConnectionPointer &conn, char *, size_t size, Comm::Flag errflag, int xerrno, void *data)
{
    GopherStateData *gopherState = (GopherStateData *) data;
    StoreEntry *entry = gopherState->entry;
    debugs(10, 5, conn << " size: " << size << " errflag: " << errflag);

    if (size > 0) {
        fd_bytes(conn->fd, size, FD_WRITE);
        statCounter.server.all.kbytes_out += size;
        statCounter.server.other.kbytes_out += size;
    }

    if (!entry->isAccepting()) {
        debugs(10, 3, "terminating due to bad " << *entry);
        // TODO: Do not abuse connection for triggering cleanup.
        gopherState->serverConn->close();
        return;
    }

    if (errflag) {
        const auto err = new ErrorState(ERR_WRITE_ERROR, Http::scServiceUnavailable, gopherState->fwd->request, gopherState->fwd->al);
        err->xerrno = xerrno;
        err->port = gopherState->fwd->request->url.port();
        err->url = xstrdup(entry->url());
        gopherState->fwd->fail(err);
        gopherState->serverConn->close();
        return;
    }

    /*
     * OK. We successfully reach remote site.  Start MIME typing
     * stuff.  Do it anyway even though request is not HTML type.
     */
    entry->buffer();

    gopherMimeCreate(gopherState);

    switch (gopherState->type_id) {

    case GOPHER_DIRECTORY:
        /* we got to convert it first */
        gopherState->conversion = GopherStateData::HTML_DIR;
        gopherState->HTML_header_added = 0;
        break;

    case GOPHER_INDEX:
        /* we got to convert it first */
        gopherState->conversion = GopherStateData::HTML_INDEX_RESULT;
        gopherState->HTML_header_added = 0;
        break;

    case GOPHER_CSO:
        /* we got to convert it first */
        gopherState->conversion = GopherStateData::HTML_CSO_RESULT;
        gopherState->cso_recno = 0;
        gopherState->HTML_header_added = 0;
        break;

    default:
        gopherState->conversion = GopherStateData::NORMAL;
        entry->flush();
    }

    /* Schedule read reply. */
    AsyncCall::Pointer call =  commCbCall(5,5, "gopherReadReply",
                                          CommIoCbPtrFun(gopherReadReply, gopherState));
    entry->delayAwareRead(conn, gopherState->replybuf, BUFSIZ, call);
}


gopherMimeCreate(GopherStateData * gopherState)
{
    StoreEntry *entry = gopherState->entry;
    const char *mime_type = NULL;
    const char *mime_enc = NULL;

    switch (gopherState->type_id) {

    case GOPHER_DIRECTORY:

    case GOPHER_INDEX:

    case GOPHER_HTML:

    case GOPHER_WWW:

    case GOPHER_CSO:
        mime_type = "text/html";
        break;

    case GOPHER_GIF:

    case GOPHER_IMAGE:

    case GOPHER_PLUS_IMAGE:
        mime_type = "image/gif";
        break;

    case GOPHER_SOUND:

    case GOPHER_PLUS_SOUND:
        mime_type = "audio/basic";
        break;

    case GOPHER_PLUS_MOVIE:
        mime_type = "video/mpeg";
        break;

    case GOPHER_MACBINHEX:

    case GOPHER_DOSBIN:

    case GOPHER_UUENCODED:

    case GOPHER_BIN:
        /* Rightnow We have no idea what it is. */
        mime_enc = mimeGetContentEncoding(gopherState->request);
        mime_type = mimeGetContentType(gopherState->request);
        if (!mime_type)
            mime_type = def_gopher_bin;
        break;

    case GOPHER_FILE:

    default:
        mime_enc = mimeGetContentEncoding(gopherState->request);
        mime_type = mimeGetContentType(gopherState->request);
        if (!mime_type)
            mime_type = def_gopher_text;
        break;
    }

    assert(entry->isEmpty());

    HttpReply *reply = new HttpReply;
    entry->buffer();
    reply->setHeaders(Http::scOkay, "Gatewaying", mime_type, -1, -1, -2);
    if (mime_enc)
        reply->header.putStr(Http::HdrType::CONTENT_ENCODING, mime_enc);

    entry->replaceHttpReply(reply);
    gopherState->reply_ = reply;
}


static void ttxtin_reset(GF_TXTIn *ctx)
{
	if (ctx->samp) gf_isom_delete_text_sample(ctx->samp);
	ctx->samp = NULL;
	if (ctx->src) gf_fclose(ctx->src);
	ctx->src = NULL;
	if (ctx->vttparser) gf_webvtt_parser_del(ctx->vttparser);
	ctx->vttparser = NULL;
	if (ctx->parser) gf_xml_dom_del(ctx->parser);
	ctx->parser = NULL;
	if (ctx->parser_working_copy) gf_xml_dom_del(ctx->parser_working_copy);
	ctx->parser_working_copy = NULL;
}

The binary MP4Box in Gpac 1.0.1 has a double-free vulnerability in the gf_text_get_utf8_line function in load_text.c, which allows attackers to cause a denial of service, even code execution and escalation of privileges.


static GF_Err gf_text_guess_format(const char *filename, u32 *fmt)
{
	char szLine[2048];
	u32 val;
	s32 uni_type;
	FILE *test = gf_fopen(filename, "rb");
	if (!test) return GF_URL_ERROR;
	uni_type = gf_text_get_utf_type(test);

	if (uni_type>1) {
		const u16 *sptr;
		char szUTF[1024];
		u32 read = (u32) gf_fread(szUTF, 1023, test);
		if ((s32) read < 0) {
			gf_fclose(test);
			return GF_IO_ERR;
		}
		szUTF[read]=0;
		sptr = (u16*)szUTF;
		/*read = (u32) */gf_utf8_wcstombs(szLine, read, &sptr);
	} else {
		val = (u32) gf_fread(szLine, 1024, test);
		if ((s32) val<0) return GF_IO_ERR;

		szLine[val]=0;
	}
	REM_TRAIL_MARKS(szLine, "\r\n\t ")

	*fmt = GF_TXTIN_MODE_NONE;
	if ((szLine[0]=='{') && strstr(szLine, "}{")) *fmt = GF_TXTIN_MODE_SUB;
	else if (szLine[0] == '<') {
		char *ext = gf_file_ext_start(filename);
		if (!strnicmp(ext, ".ttxt", 5)) *fmt = GF_TXTIN_MODE_TTXT;
		else if (!strnicmp(ext, ".ttml", 5)) *fmt = GF_TXTIN_MODE_TTML;
		ext = strstr(szLine, "?>");
		if (ext) ext += 2;
		if (ext && !ext[0]) {
			if (!gf_fgets(szLine, 2048, test))
				szLine[0] = '\0';
		}
		if (strstr(szLine, "x-quicktime-tx3g") || strstr(szLine, "text3GTrack")) *fmt = GF_TXTIN_MODE_TEXML;
		else if (strstr(szLine, "TextStream")) *fmt = GF_TXTIN_MODE_TTXT;
		else if (strstr(szLine, "tt")) *fmt = GF_TXTIN_MODE_TTML;
	}
	else if (strstr(szLine, "WEBVTT") )
		*fmt = GF_TXTIN_MODE_WEBVTT;
	else if (strstr(szLine, " --> ") )
		*fmt = GF_TXTIN_MODE_SRT; /* might want to change the default to WebVTT */

	else if (!strncmp(szLine, "FWS", 3) || !strncmp(szLine, "CWS", 3))
		*fmt = GF_TXTIN_MODE_SWF_SVG;

	gf_fclose(test);
	return GF_OK;
}


static void ttxt_parse_text_box(GF_XMLNode *n, GF_BoxRecord *box)
{
	u32 i=0;
	GF_XMLAttribute *att;
	memset(box, 0, sizeof(GF_BoxRecord));
	while ( (att=(GF_XMLAttribute *)gf_list_enum(n->attributes, &i))) {
		if (!stricmp(att->name, "top")) box->top = atoi(att->value);
		else if (!stricmp(att->name, "bottom")) box->bottom = atoi(att->value);
		else if (!stricmp(att->name, "left")) box->left = atoi(att->value);
		else if (!stricmp(att->name, "right")) box->right = atoi(att->value);
	}
}


static GF_Err gf_text_process_swf(GF_Filter *filter, GF_TXTIn *ctx)
{
	GF_Err e=GF_OK;

	if (!ctx->is_setup) {
		ctx->is_setup = GF_TRUE;
		return gf_text_swf_setup(filter, ctx);
	}
	if (!ctx->opid) return GF_NOT_SUPPORTED;

	if (ctx->seek_state==1) {
		ctx->seek_state = 2;
		gf_swf_reader_del(ctx->swf_parse);
		ctx->swf_parse = gf_swf_reader_new(NULL, ctx->file_name);
		gf_swf_read_header(ctx->swf_parse);
		gf_swf_reader_set_user_mode(ctx->swf_parse, ctx, swf_svg_add_iso_sample, swf_svg_add_iso_header);
	}

	ctx->do_suspend = GF_FALSE;
	/*parse all tags*/
	while (e == GF_OK) {
		e = swf_parse_tag(ctx->swf_parse);
		if (ctx->do_suspend) return GF_OK;
	}
	if (e==GF_EOS) {
		if (ctx->swf_parse->finalize) {
			ctx->swf_parse->finalize(ctx->swf_parse);
			ctx->swf_parse->finalize = NULL;
		}
	}
	return e;
}


#else
const GF_FilterRegister *txtin_register(GF_FilterSession *session)
{
	return NULL;


void txtin_finalize(GF_Filter *filter)
{
	GF_TXTIn *ctx = gf_filter_get_udta(filter);

	ttxtin_reset(ctx);
	if (ctx->bs_w) gf_bs_del(ctx->bs_w);

	if (ctx->text_descs) {
		while (gf_list_count(ctx->text_descs)) {
			GF_PropertyValue *p = gf_list_pop_back(ctx->text_descs);
			gf_free(p->value.data.ptr);
			gf_free(p);
		}
		gf_list_del(ctx->text_descs);
	}
#ifndef GPAC_DISABLE_SWF_IMPORT
	gf_swf_reader_del(ctx->swf_parse);
#endif

	if (ctx->intervals) {
		ttml_reset_intervals(ctx);
		gf_list_del(ctx->intervals);
	}
	if (ctx->ttml_resources) {
		while (gf_list_count(ctx->ttml_resources)) {
			TTMLRes *ires = gf_list_pop_back(ctx->ttml_resources);
			gf_free(ires->data);
			gf_free(ires);
		}
		gf_list_del(ctx->ttml_resources);
	}
	if (ctx->div_nodes_list)
		gf_list_del(ctx->div_nodes_list);

	if (ctx->file_name) gf_free(ctx->file_name);
}


static GF_Err gf_text_process_swf(GF_Filter *filter, GF_TXTIn *ctx)
{
	GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("Warning: GPAC was compiled without SWF import support, can't import file.\n"));
	return GF_NOT_SUPPORTED;
}


static void txtin_process_send_text_sample(GF_TXTIn *ctx, GF_TextSample *txt_samp, u64 ts, u32 duration, Bool is_rap)
{
	GF_FilterPacket *dst_pck;
	u8 *pck_data;
	u32 size;

	if (ctx->seek_state==2) {
		Double end = (Double) (ts+duration);
		end /= 1000;
		if (end < ctx->start_range) return;
		ctx->seek_state = 0;
	}

	size = gf_isom_text_sample_size(txt_samp);

	dst_pck = gf_filter_pck_new_alloc(ctx->opid, size, &pck_data);
	if (!dst_pck) return;

	gf_bs_reassign_buffer(ctx->bs_w, pck_data, size);
	gf_isom_text_sample_write_bs(txt_samp, ctx->bs_w);

	ts = gf_timestamp_rescale(ts, 1000, ctx->timescale);
	duration = (u32) gf_timestamp_rescale(duration, 1000, ctx->timescale);

	gf_filter_pck_set_sap(dst_pck, is_rap ? GF_FILTER_SAP_1 : GF_FILTER_SAP_NONE);
	gf_filter_pck_set_cts(dst_pck, ts);
	gf_filter_pck_set_duration(dst_pck, duration);

	gf_filter_pck_send(dst_pck);
}


static GF_Err txtin_process_webvtt(GF_Filter *filter, GF_TXTIn *ctx)
{
	GF_Err e;

	if (!ctx->is_setup) {
		ctx->is_setup = GF_TRUE;
		return txtin_webvtt_setup(filter, ctx);
	}
	if (!ctx->vttparser) return GF_NOT_SUPPORTED;
	if (ctx->seek_state==1) {
		ctx->seek_state = 2;
		gf_webvtt_parser_restart(ctx->vttparser);
	}

	e = gf_webvtt_parser_parse(ctx->vttparser);
	if (e < GF_OK) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] WebVTT process error %s\n", gf_error_to_string(e) ));
	}
	return e;
}


static GF_Err ttml_rewrite_timestamp(GF_TXTIn *ctx, s64 ttml_zero, GF_XMLAttribute *att, s64 *value, Bool *drop)
{
	u64 v;
	char szTS[21];
	u32 h, m, s, ms;
	*value = ttml_get_timestamp(ctx, att->value);
	if (!ttml_zero)
		return GF_OK;

	if (*value < ttml_zero) {
		*drop = GF_TRUE;
		return GF_OK;
	}

	*value -= ttml_zero;
	v = (u64) (*value / 1000);
	h = (u32) (v / 3600);
	m = (u32) (v - h*60) / 60;
	s = (u32) (v - h*3600 - m*60);
	ms = (*value) % 1000;

	snprintf(szTS, 20, "%02d:%02d:%02d.%03d", h, m, s, ms);
	szTS[20] = 0;
	gf_free(att->value);
	att->value = gf_strdup(szTS);
	return GF_OK;
}


static GF_Err swf_svg_add_iso_sample(void *user, const u8 *data, u32 length, u64 timestamp, Bool isRap)
{
	GF_FilterPacket *pck;
	u8 *pck_data;
	GF_TXTIn *ctx = (GF_TXTIn *)user;

	if (ctx->seek_state==2) {
		Double ts = (Double) timestamp;
		ts/=1000;
		if (ts<ctx->start_range) return GF_OK;
		ctx->seek_state = 0;
	}

	pck = gf_filter_pck_new_alloc(ctx->opid, length, &pck_data);
	if (pck) {
		memcpy(pck_data, data, length);
		gf_filter_pck_set_cts(pck, (u64) (ctx->timescale*timestamp/1000) );
		gf_filter_pck_set_sap(pck, isRap ? GF_FILTER_SAP_1 : GF_FILTER_SAP_NONE);
		gf_filter_pck_set_framing(pck, GF_TRUE, GF_FALSE);

		gf_filter_pck_send(pck);
	}

	if (gf_filter_pid_would_block(ctx->opid))
		ctx->do_suspend = GF_TRUE;
	return GF_OK;
}


static GF_Err txtin_process_ttxt(GF_Filter *filter, GF_TXTIn *ctx)
{
	u32 j, k;
	GF_XMLNode *root, *ext;

	if (!ctx->is_setup) {
		ctx->is_setup = GF_TRUE;
		return txtin_setup_ttxt(filter, ctx);
	}
	if (!ctx->opid) return GF_NON_COMPLIANT_BITSTREAM;
	if (!ctx->playstate) return GF_OK;
	else if (ctx->playstate==2) return GF_EOS;

	if (ctx->seek_state==1) {
		ctx->seek_state = 2;
		ctx->cur_child_idx = 0;
	}
	root = gf_xml_dom_get_root(ctx->parser);

	for (; ctx->cur_child_idx < ctx->nb_children; ctx->cur_child_idx++) {
		GF_TextSample * samp;
		u32 ts, descIndex;
		Bool has_text = GF_FALSE;
		GF_XMLAttribute *att;
		GF_XMLNode *node = (GF_XMLNode*) gf_list_get(root->content, ctx->cur_child_idx);

		if (node->type) {
			continue;
		}
		/*sample text*/
		else if (strcmp(node->name, "TextSample")) continue;

		samp = gf_isom_new_text_sample();
		ts = 0;
		descIndex = 1;
		ctx->last_sample_empty = GF_TRUE;

		j=0;
		while ( (att=(GF_XMLAttribute*)gf_list_enum(node->attributes, &j))) {
			if (!strcmp(att->name, "sampleTime")) {
				u32 h, m, s, ms;
				if (sscanf(att->value, "%u:%u:%u.%u", &h, &m, &s, &ms) == 4) {
					ts = (h*3600 + m*60 + s)*1000 + ms;
				} else {
					ts = (u32) (atof(att->value) * 1000);
				}
			}
			else if (!strcmp(att->name, "sampleDescriptionIndex")) descIndex = atoi(att->value);
			else if (!strcmp(att->name, "text")) {
				u32 len;
				char *str = ttxt_parse_string(att->value, GF_TRUE);
				len = (u32) strlen(str);
				gf_isom_text_add_text(samp, str, len);
				ctx->last_sample_empty = len ? GF_FALSE : GF_TRUE;
				has_text = GF_TRUE;
			}
			else if (!strcmp(att->name, "scrollDelay")) gf_isom_text_set_scroll_delay(samp, (u32) (1000*atoi(att->value)));
			else if (!strcmp(att->name, "highlightColor")) gf_isom_text_set_highlight_color(samp, ttxt_get_color(att->value));
			else if (!strcmp(att->name, "wrap") && !strcmp(att->value, "Automatic")) gf_isom_text_set_wrap(samp, 0x01);
		}

		/*get all modifiers*/
		j=0;
		while ( (ext=(GF_XMLNode*)gf_list_enum(node->content, &j))) {
			if (!has_text && (ext->type==GF_XML_TEXT_TYPE)) {
				u32 len;
				char *str = ttxt_parse_string(ext->name, GF_FALSE);
				len = (u32) strlen(str);
				gf_isom_text_add_text(samp, str, len);
				ctx->last_sample_empty = len ? GF_FALSE : GF_TRUE;
				has_text = GF_TRUE;
			}
			if (ext->type) continue;

			if (!stricmp(ext->name, "Style")) {
				GF_StyleRecord r;
				ttxt_parse_text_style(ctx, ext, &r);
				gf_isom_text_add_style(samp, &r);
			}
			else if (!stricmp(ext->name, "TextBox")) {
				GF_BoxRecord r;
				ttxt_parse_text_box(ext, &r);
				gf_isom_text_set_box(samp, r.top, r.left, r.bottom, r.right);
			}
			else if (!stricmp(ext->name, "Highlight")) {
				u16 start, end;
				start = end = 0;
				k=0;
				while ( (att=(GF_XMLAttribute *)gf_list_enum(ext->attributes, &k))) {
					if (!strcmp(att->name, "fromChar")) start = atoi(att->value);
					else if (!strcmp(att->name, "toChar")) end = atoi(att->value);
				}
				gf_isom_text_add_highlight(samp, start, end);
			}
			else if (!stricmp(ext->name, "Blinking")) {
				u16 start, end;
				start = end = 0;
				k=0;
				while ( (att=(GF_XMLAttribute *)gf_list_enum(ext->attributes, &k))) {
					if (!strcmp(att->name, "fromChar")) start = atoi(att->value);
					else if (!strcmp(att->name, "toChar")) end = atoi(att->value);
				}
				gf_isom_text_add_blink(samp, start, end);
			}
			else if (!stricmp(ext->name, "HyperLink")) {
				u16 start, end;
				char *url, *url_tt;
				start = end = 0;
				url = url_tt = NULL;
				k=0;
				while ( (att=(GF_XMLAttribute *)gf_list_enum(ext->attributes, &k))) {
					if (!strcmp(att->name, "fromChar")) start = atoi(att->value);
					else if (!strcmp(att->name, "toChar")) end = atoi(att->value);
					else if (!strcmp(att->name, "URL")) url = gf_strdup(att->value);
					else if (!strcmp(att->name, "URLToolTip")) url_tt = gf_strdup(att->value);
				}
				gf_isom_text_add_hyperlink(samp, url, url_tt, start, end);
				if (url) gf_free(url);
				if (url_tt) gf_free(url_tt);
			}
			else if (!stricmp(ext->name, "Karaoke")) {
				u32 startTime;
				GF_XMLNode *krok;
				startTime = 0;
				k=0;
				while ( (att=(GF_XMLAttribute *)gf_list_enum(ext->attributes, &k))) {
					if (!strcmp(att->name, "startTime")) startTime = (u32) (1000*atof(att->value));
				}
				gf_isom_text_add_karaoke(samp, startTime);
				k=0;
				while ( (krok=(GF_XMLNode*)gf_list_enum(ext->content, &k))) {
					u16 start, end;
					u32 endTime, m;
					if (krok->type) continue;
					if (strcmp(krok->name, "KaraokeRange")) continue;
					start = end = 0;
					endTime = 0;
					m=0;
					while ( (att=(GF_XMLAttribute *)gf_list_enum(krok->attributes, &m))) {
						if (!strcmp(att->name, "fromChar")) start = atoi(att->value);
						else if (!strcmp(att->name, "toChar")) end = atoi(att->value);
						else if (!strcmp(att->name, "endTime")) endTime = (u32) (1000*atof(att->value));
					}
					gf_isom_text_set_karaoke_segment(samp, endTime, start, end);
				}
			}
		}

		if (!descIndex) descIndex = 1;
		if (descIndex != ctx->last_desc_idx) {
			GF_PropertyValue *dcd;
			ctx->last_desc_idx = descIndex;
			dcd = gf_list_get(ctx->text_descs, descIndex-1);
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DECODER_CONFIG, dcd);
		}

		/*in MP4 we must start at T=0, so add an empty sample*/
		if (ts && ctx->first_samp) {
			GF_TextSample * firstsamp = gf_isom_new_text_sample();
			txtin_process_send_text_sample(ctx, firstsamp, 0, 0, GF_TRUE);
			gf_isom_delete_text_sample(firstsamp);
		}
		ctx->first_samp = GF_FALSE;

		txtin_process_send_text_sample(ctx, samp, ts, 0, GF_TRUE);

		gf_isom_delete_text_sample(samp);

		if (ctx->last_sample_empty) {
			ctx->last_sample_duration = ts - ctx->last_sample_duration;
		} else {
			ctx->last_sample_duration = ts;
		}

		if (gf_filter_pid_would_block(ctx->opid)) {
			ctx->cur_child_idx++;
			return GF_OK;
		}
	}

	if (ctx->last_sample_empty) {
		//this is a bit ugly, in regular streaming mode we don't want to remove empty samples
		//howvere the last one can be removed, adjusting the duration of the previous one.
		//doing this here is problematic if the loader is sent a new ttxt file, we would have a cue termination sample
		//we therefore share that info through pid, and let the final user (muxer& co) decide what to do
		gf_filter_pid_set_info_str( ctx->opid, "ttxt:rem_last", &PROP_BOOL(GF_TRUE) );
		gf_filter_pid_set_info_str( ctx->opid, "ttxt:last_dur", &PROP_UINT((u32) ctx->last_sample_duration) );
	}

	return GF_EOS;
}


static GF_Err txtin_process(GF_Filter *filter)
{
	GF_TXTIn *ctx = gf_filter_get_udta(filter);
	GF_FilterPacket *pck;
	GF_Err e;
	Bool start, end;
	pck = gf_filter_pid_get_packet(ctx->ipid);
	if (!pck) {
		return GF_OK;
	}
	gf_filter_pck_get_framing(pck, &start, &end);
	if (!end) {
		gf_filter_pid_drop_packet(ctx->ipid);
		return GF_OK;
	}
	//file is loaded

	e = ctx->text_process(filter, ctx);

	if (e==GF_EOS) {
		//keep input alive until end of stream, so that we keep getting called
		gf_filter_pid_drop_packet(ctx->ipid);
		if (gf_filter_pid_is_eos(ctx->ipid))
			gf_filter_pid_set_eos(ctx->opid);
	}
	return e;
}


static void ttxt_dom_progress(void *cbk, u64 cur_samp, u64 count)
{
	GF_TXTIn *ctx = (GF_TXTIn *)cbk;
	ctx->end = count;
}


static void ebu_ttd_remove_samples(GF_XMLNode *root, GF_XMLNode **out_body_node)
{
	u32 idx = 0;
	GF_XMLNode *node = NULL;
	*out_body_node = NULL;
	while ( (node = (GF_XMLNode*)gf_list_enum(root->content, &idx))) {
		if (!strcmp(node->name, "body")) {
			GF_XMLNode *body_node;
			u32 body_idx = 0;
			*out_body_node = node;
			while ( (body_node = (GF_XMLNode*)gf_list_enum(node->content, &body_idx))) {
				if (!strcmp(body_node->name, "div")) {
					u32 body_num;
					body_num = gf_list_count(body_node->content);
					while (body_num--) {
						GF_XMLNode *content_node = (GF_XMLNode*)gf_list_get(body_node->content, 0);
						assert(gf_list_find(body_node->content, content_node) == 0);
						gf_list_rem(body_node->content, 0);
						gf_xml_dom_node_del(content_node);
					}
				}
			}
			return;
		}
	}
}


static GF_Err gf_text_swf_setup(GF_Filter *filter, GF_TXTIn *ctx)
{
	GF_Err e;
	u32 ID;

	ctx->swf_parse = gf_swf_reader_new(NULL, ctx->file_name);
	e = gf_swf_read_header(ctx->swf_parse);
	if (e) return e;
	gf_swf_reader_set_user_mode(ctx->swf_parse, ctx, swf_svg_add_iso_sample, swf_svg_add_iso_header);

	if (!ctx->timescale) ctx->timescale = 1000;

	if (!ctx->opid) ctx->opid = gf_filter_pid_new(filter);
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_STREAM_TYPE, &PROP_UINT(GF_STREAM_TEXT) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CODECID, &PROP_UINT(GF_CODECID_SIMPLE_TEXT) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_TIMESCALE, &PROP_UINT(ctx->timescale) );
//	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DOWN_SIZE, &PROP_UINT(file_size) );

	//patch for old arch
	ctx->width = FIX2INT(ctx->swf_parse->width);
	ctx->height = FIX2INT(ctx->swf_parse->height);
	if (!ctx->width && !ctx->height) {
		ctx->width = 400;
		ctx->height = 60;
	}
	ID = 1;
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ID, &PROP_UINT(ID) );
	if (ctx->width) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_WIDTH, &PROP_UINT(ctx->width) );
	if (ctx->height) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_HEIGHT, &PROP_UINT(ctx->height) );
	if (ctx->zorder) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ZORDER, &PROP_SINT(ctx->zorder) );
	if (ctx->lang) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_LANGUAGE, &PROP_STRING( ctx->lang) );

	gf_filter_pid_set_property_str(ctx->opid, "meta:mime", &PROP_STRING("image/svg+xml") );

#ifndef GPAC_DISABLE_SVG
	GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] swf -> svg not fully migrated, using SWF flags 0 and no flatten angle. Patch welcome\n"));
	e = swf_to_svg_init(ctx->swf_parse, 0, 0);
#endif

	//SWF->BIFS is handled in ctx loader, no need to define it here
	txtin_probe_duration(ctx);

	return e;
}


static Bool ttml_check_range(TTMLInterval *interval, s64 ts_begin, s64 ts_end)
{
	//if in current interval, push node
	if ((ts_begin != -1) && (ts_end != -1) && ((ts_begin>=interval->begin) && (ts_end<=interval->end))
	) {
		return GF_TRUE;
	}
	//begin not set, end set: in range if end less than interval end range
	else if ((ts_begin==-1) && (ts_end != -1) && (ts_end<=interval->end)) {
		return GF_TRUE;
	}
	//begin set, end not set: in range if begin greater than interval begin range
	else if ((ts_begin!=-1) && (ts_end==-1) && (ts_begin>=interval->begin)) {
		return GF_TRUE;
	}
	return GF_FALSE;
}


static char *ttxt_parse_string(char *str, Bool strip_lines)
{
	u32 i=0;
	u32 k=0;
	u32 len = (u32) strlen(str);
	u32 state = 0;

	if (!strip_lines) {
		for (i=0; i<len; i++) {
			if ((str[i] == '\r') && (str[i+1] == '\n')) {
				i++;
			}
			str[k] = str[i];
			k++;
		}
		str[k]=0;
		return str;
	}

	if (str[0]!='\'') return str;
	for (i=0; i<len; i++) {
		if (str[i] == '\'') {

			if (!state) {
				if (k) {
					str[k]='\n';
					k++;
				}
				state = 1; //!state;
			} else {
				if ( (i+1==len) ||
				        ((str[i+1]==' ') || (str[i+1]=='\n') || (str[i+1]=='\r') || (str[i+1]=='\t') || (str[i+1]=='\''))
				   ) {
					state = !state;
				} else {
					str[k] = str[i];
					k++;
				}
			}
		} else if (state) {
			str[k] = str[i];
			k++;
		}
	}
	str[k]=0;
	return str;
}


static GF_Err gf_text_process_sub(GF_Filter *filter, GF_TXTIn *ctx)
{
	u32 i, j, len, line;
	GF_TextSample *samp;
	Double ts_scale;
	char szLine[2048], szTime[20], szText[2048];

	//same setup as for srt
	if (!ctx->is_setup) {
		ctx->is_setup = GF_TRUE;
		return txtin_setup_srt(filter, ctx);
	}
	if (!ctx->opid) return GF_NOT_SUPPORTED;
	if (!ctx->playstate) return GF_OK;
	else if (ctx->playstate==2) return GF_EOS;

	if (ctx->seek_state==1) {
		ctx->seek_state = 2;
		gf_fseek(ctx->src, 0, SEEK_SET);
	}

	if (ctx->fps.den && ctx->fps.num) {
		ts_scale = ((Double) ctx->fps.num) / ctx->fps.den;
	} else {
		ts_scale = 25;
	}

	line = 0;

	while (1) {
		char *sOK = gf_text_get_utf8_line(szLine, 2048, ctx->src, ctx->unicode_type);
		if (!sOK) break;

		REM_TRAIL_MARKS(szLine, "\r\n\t ")

		line++;
		len = (u32) strlen(szLine);
		if (!len) continue;

		i=0;
		if (szLine[i] != '{') {
			GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] Bad SUB file (line %d): expecting \"{\" got \"%c\"\n", line, szLine[i]));
			continue;
		}
		while (szLine[i+1] && szLine[i+1]!='}') {
			szTime[i] = szLine[i+1];
			i++;
			if (i>=19)
				break;
		}
		szTime[i] = 0;
		ctx->start = atoi(szTime);
		if (ctx->start < ctx->end) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] corrupted SUB frame (line %d) - starts (at %d ms) before end of previous one (%d ms) - adjusting time stamps\n", line, ctx->start, ctx->end));
			ctx->start = ctx->end;
		}
		j=i+2;
		i=0;
		if (szLine[i+j] != '{') {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Bad SUB file - expecting \"{\" got \"%c\"\n", szLine[i]));
			continue;
		}
		while (szLine[i+1+j] && szLine[i+1+j]!='}') {
			szTime[i] = szLine[i+1+j];
			i++;
		}
		szTime[i] = 0;
		ctx->end = atoi(szTime);
		j+=i+2;

		if (ctx->start > ctx->end) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] corrupted SUB frame (line %d) - ends (at %d ms) before start of current frame (%d ms) - skipping\n", line, ctx->end, ctx->start));
			continue;
		}

		if (ctx->start && ctx->first_samp) {
			samp = gf_isom_new_text_sample();
			txtin_process_send_text_sample(ctx, samp, 0, (u32) (ts_scale*ctx->start), GF_TRUE);
			ctx->first_samp = GF_FALSE;
			gf_isom_delete_text_sample(samp);
		}

		for (i=j; i<len; i++) {
			if (szLine[i]=='|') {
				szText[i-j] = '\n';
			} else {
				szText[i-j] = szLine[i];
			}
		}
		szText[i-j] = 0;

		if (ctx->prev_end) {
			samp = gf_isom_new_text_sample();
			txtin_process_send_text_sample(ctx, samp, (u64) (ts_scale*(s64)ctx->prev_end), (u32) (ts_scale*(ctx->start - ctx->prev_end)), GF_TRUE);
			gf_isom_delete_text_sample(samp);
		}

		samp = gf_isom_new_text_sample();
		gf_isom_text_add_text(samp, szText, (u32) strlen(szText) );
		txtin_process_send_text_sample(ctx, samp, (u64) (ts_scale*(s64)ctx->start), (u32) (ts_scale*(ctx->end - ctx->start)), GF_TRUE);
		gf_isom_delete_text_sample(samp);

		ctx->prev_end = ctx->end;

		gf_filter_pid_set_info(ctx->opid, GF_PROP_PID_DOWN_BYTES, &PROP_LONGUINT( gf_ftell(ctx->src )) );

		if (gf_filter_pid_would_block(ctx->opid))
			return GF_OK;
	}
	/*final flush*/
	if (ctx->end && !ctx->noflush) {
		samp = gf_isom_new_text_sample();
		txtin_process_send_text_sample(ctx, samp, (u64) (ts_scale*(s64)ctx->end), 0, GF_TRUE);
		gf_isom_delete_text_sample(samp);
	}

	gf_filter_pid_set_info_str( ctx->opid, "ttxt:last_dur", &PROP_UINT(0) );

	return GF_EOS;
}


static GF_Err ttml_push_interval(GF_TXTIn *ctx, s64 begin, s64 end, TTMLInterval **out_interval)
{
	u32 i;
	TTMLInterval *interval;
	if (begin==-1) return GF_OK;
	if (end==-1) return GF_OK;

	if (end < begin) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TTML EBU-TTD] invalid timings: \"begin\"="LLD" , \"end\"="LLD". Abort.\n", begin, end));
		return GF_NON_COMPLIANT_BITSTREAM;
	}

	interval = NULL;
	for (i=0; i<gf_list_count(ctx->intervals); i++) {
		interval = gf_list_get(ctx->intervals, i);

		//generate a single sample for the input, merge interval
		if (ctx->ttml_dur>=0) {
			if (interval->begin > begin) interval->begin = begin;
			if (interval->end < end) interval->end = end;
			*out_interval = interval;
			return GF_OK;
		}
		//contained, do nothing
		if ((begin>=interval->begin) && (end<=interval->end)) {
			*out_interval = interval;
			return GF_OK;
		}
		//not overlapping
		if ((end < interval->begin) || (begin > interval->end))
			continue;

		//new interval starts before current and end after, remove current and push extended interval
		if ((begin < interval->end) && (end > interval->end)) {
			if (begin>interval->begin)
				begin = interval->begin;
			gf_list_rem(ctx->intervals, i);
			gf_free(interval);
			return ttml_push_interval(ctx, begin, end, out_interval);
		}
		//new interval starts before current and end before, remove current and push rewinded
		if ((begin < interval->end) && (end <= interval->end)) {
			end = interval->end;
			if (begin>interval->begin)
				begin = interval->begin;
			gf_list_rem(ctx->intervals, i);
			gf_free(interval);
			return ttml_push_interval(ctx, begin, end, out_interval);
		}
	}
	//need a new interval
	GF_SAFEALLOC(interval, TTMLInterval);
	interval->begin = begin;
	interval->end = end;
	*out_interval = interval;

	for (i=0; i<gf_list_count(ctx->intervals); i++) {
		TTMLInterval *an_interval = gf_list_get(ctx->intervals, i);
		if (an_interval->begin > interval->begin) {
			return gf_list_insert(ctx->intervals, interval, i);
		}
	}
	return gf_list_add(ctx->intervals, interval);
}


char *gf_text_get_utf8_line(char *szLine, u32 lineSize, FILE *txt_in, s32 unicode_type)
{
	u32 i, j, len;
	char *sOK;
	char szLineConv[2048];
	unsigned short *sptr;

	memset(szLine, 0, sizeof(char)*lineSize);
	sOK = gf_fgets(szLine, lineSize, txt_in);
	if (!sOK) return NULL;
	if (unicode_type<=1) {
		j=0;
		len = (u32) strlen(szLine);
		for (i=0; i<len; i++) {
			if (!unicode_type && (szLine[i] & 0x80)) {
				/*non UTF8 (likely some win-CP)*/
				if ((szLine[i+1] & 0xc0) != 0x80) {
					szLineConv[j] = 0xc0 | ( (szLine[i] >> 6) & 0x3 );
					j++;
					szLine[i] &= 0xbf;
				}
				/*UTF8 2 bytes char*/
				else if ( (szLine[i] & 0xe0) == 0xc0) {
					szLineConv[j] = szLine[i];
					i++;
					j++;
				}
				/*UTF8 3 bytes char*/
				else if ( (szLine[i] & 0xf0) == 0xe0) {
					szLineConv[j] = szLine[i];
					i++;
					j++;
					szLineConv[j] = szLine[i];
					i++;
					j++;
				}
				/*UTF8 4 bytes char*/
				else if ( (szLine[i] & 0xf8) == 0xf0) {
					szLineConv[j] = szLine[i];
					i++;
					j++;
					szLineConv[j] = szLine[i];
					i++;
					j++;
					szLineConv[j] = szLine[i];
					i++;
					j++;
				} else {
					i+=1;
					continue;
				}
			}
			szLineConv[j] = szLine[i];
			j++;
		}
		szLineConv[j] = 0;
		strcpy(szLine, szLineConv);
		return sOK;
	}

#ifdef GPAC_BIG_ENDIAN
	if (unicode_type==3)
#else
	if (unicode_type==2)
#endif
	{
		i=0;
		while (1) {
			char c;
			if (!szLine[i] && !szLine[i+1]) break;
			c = szLine[i+1];
			szLine[i+1] = szLine[i];
			szLine[i] = c;
			i+=2;
		}
	}
	sptr = (u16 *)szLine;
	i = (u32) gf_utf8_wcstombs(szLineConv, 2048, (const unsigned short **) &sptr);
	szLineConv[i] = 0;
	strcpy(szLine, szLineConv);
	/*this is ugly indeed: since input is UTF16-LE, there are many chances the gf_fgets never reads the \0 after a \n*/
	if (unicode_type==3) gf_fgetc(txt_in);
	return sOK;
}


static void tx3g_parse_text_box(GF_XMLNode *n, GF_BoxRecord *box)
{
	u32 i=0;
	GF_XMLAttribute *att;
	memset(box, 0, sizeof(GF_BoxRecord));
	while ((att=(GF_XMLAttribute *)gf_list_enum(n->attributes, &i))) {
		if (!stricmp(att->name, "x")) box->left = atoi(att->value);
		else if (!stricmp(att->name, "y")) box->top = atoi(att->value);
		else if (!stricmp(att->name, "height")) box->bottom = atoi(att->value);
		else if (!stricmp(att->name, "width")) box->right = atoi(att->value);
	}
}


static void ttml_reset_intervals(GF_TXTIn *ctx)
{
	while (gf_list_count(ctx->intervals)) {
		TTMLInterval *ival = gf_list_pop_back(ctx->intervals);
		if (ival->resources) {
			while (gf_list_count(ival->resources)) {
				TTMLRes *ires = gf_list_pop_back(ival->resources);
				if (!ires->global) {
					gf_free(ires->data);
					gf_free(ires);
				}
			}
			gf_list_del(ival->resources);
		}
		gf_free(ival);
	}
}


static void gf_webvtt_import_header(void *user, const char *config)
{
	GF_TXTIn *ctx = (GF_TXTIn *)user;
	if (!ctx->hdr_parsed) {
		gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DECODER_CONFIG, &PROP_DATA((char *) config, (u32) (1+strlen(config)) ) );
		ctx->hdr_parsed = GF_TRUE;
		gf_webvtt_parser_suspend(ctx->vttparser);
	}
}


static GF_Err txtin_process_srt(GF_Filter *filter, GF_TXTIn *ctx)
{
	u32 i;
	u32 sh, sm, ss, sms, eh, em, es, ems, txt_line, char_len, char_line, j, rem_styles;
	Bool set_start_char, set_end_char, rem_color;
	u32 line, len;
	char szLine[2048], szText[2048], *ptr;
	unsigned short uniLine[5000], uniText[5000], *sptr;

	if (!ctx->is_setup) {
		ctx->is_setup = GF_TRUE;
		return txtin_setup_srt(filter, ctx);
	}
	if (!ctx->opid) return GF_NOT_SUPPORTED;
	if (!ctx->playstate) return GF_OK;
	else if (ctx->playstate==2) return GF_EOS;

	txt_line = 0;
	set_start_char = set_end_char = GF_FALSE;
	char_len = 0;

	if (ctx->seek_state == 1) {
		ctx->seek_state = 2;
		gf_fseek(ctx->src, 0, SEEK_SET);
	}

	while (1) {
		Bool is_empty = GF_FALSE;
		char *sOK = gf_text_get_utf8_line(szLine, 2048, ctx->src, ctx->unicode_type);

		if (sOK) {
			REM_TRAIL_MARKS(szLine, "\r\n\t ")

			if (ctx->unicode_type<=1) is_empty = strlen(szLine) ? GF_FALSE : GF_TRUE;
			else is_empty =  (!szLine[0] && !szLine[1]) ? GF_TRUE : GF_FALSE;
		}

		if (!sOK || is_empty) {
			u32 utf_inc = (ctx->unicode_type<=1) ? 1 : 2;
			u32 nb_empty = utf_inc;
			u32 pos = (u32) gf_ftell(ctx->src);
			if (ctx->state) {
				while (!gf_feof(ctx->src)) {
					sOK = gf_text_get_utf8_line(szLine+nb_empty, 2048-nb_empty, ctx->src, ctx->unicode_type);
					if (sOK) REM_TRAIL_MARKS((szLine+nb_empty), "\r\n\t ")

					if (!sOK) {
						gf_fseek(ctx->src, pos, SEEK_SET);
						break;
					} else if (!strlen(szLine+nb_empty)) {
						nb_empty+=utf_inc;
						continue;
					} else if (	sscanf(szLine+nb_empty, "%u", &line) == 1) {
						gf_fseek(ctx->src, pos, SEEK_SET);
						break;
					} else {
						u32 k;
						for (k=0; k<nb_empty; k++) szLine[k] = '\n';
						goto force_line;
					}
				}
			}
			ctx->style.style_flags = 0;
			ctx->style.text_color = 0xFFFFFFFF;
			ctx->style.startCharOffset = ctx->style.endCharOffset = 0;
			if (txt_line) {
				if (ctx->prev_end && (ctx->start != ctx->prev_end) && (ctx->state<=2)) {
					GF_TextSample * empty_samp = gf_isom_new_text_sample();
					txtin_process_send_text_sample(ctx, empty_samp, ctx->prev_end, (u32) (ctx->start - ctx->prev_end), GF_TRUE );
					gf_isom_delete_text_sample(empty_samp);
				}

				if (ctx->state<=2) {
					txtin_process_send_text_sample(ctx, ctx->samp,  ctx->start, (u32) (ctx->end -  ctx->start), GF_TRUE);
					ctx->prev_end = ctx->end;
				}
				txt_line = 0;
				char_len = 0;
				set_start_char = set_end_char = GF_FALSE;
				ctx->style.startCharOffset = ctx->style.endCharOffset = 0;
				gf_isom_text_reset(ctx->samp);

				gf_filter_pid_set_info(ctx->opid, GF_PROP_PID_DOWN_BYTES, &PROP_LONGUINT( gf_ftell(ctx->src )) );
			}
			ctx->state = 0;
			if (!sOK) break;
			continue;
		}

force_line:
		switch (ctx->state) {
		case 0:
			if (sscanf(szLine, "%u", &line) != 1) {
				GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] Bad SRT formatting - expecting number got \"%s\"\n", szLine));
				break;
			}
			if (line != ctx->curLine + 1) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Corrupted SRT frame %d after frame %d\n", line, ctx->curLine));
			}
			ctx->curLine = line;
			ctx->state = 1;
			break;
		case 1:
			if (sscanf(szLine, "%u:%u:%u,%u --> %u:%u:%u,%u", &sh, &sm, &ss, &sms, &eh, &em, &es, &ems) != 8) {
				if (sscanf(szLine, "%u:%u:%u.%u --> %u:%u:%u.%u", &sh, &sm, &ss, &sms, &eh, &em, &es, &ems) != 8) {
					sh = eh = 0;
					if (sscanf(szLine, "%u:%u,%u --> %u:%u,%u", &sm, &ss, &sms, &em, &es, &ems) != 6) {
						if (sscanf(szLine, "%u:%u.%u --> %u:%u.%u", &sm, &ss, &sms, &em, &es, &ems) != 6) {
							GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Error scanning SRT frame %d timing\n", ctx->curLine));
				    		ctx->state = 0;
							break;
						}
					}
				}
			}
			ctx->start = (3600*sh + 60*sm + ss)*1000 + sms;
			if (ctx->start < ctx->end) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Overlapping SRT frame %d - starts "LLD" ms is before end of previous one "LLD" ms - adjusting time stamps\n", ctx->curLine, ctx->start, ctx->end));
				ctx->start = ctx->end;
			}

			ctx->end = (3600*eh + 60*em + es)*1000 + ems;
			/*make stream start at 0 by inserting a fake AU*/
			if (ctx->first_samp && (ctx->start > 0)) {
				txtin_process_send_text_sample(ctx, ctx->samp, 0, (u32) ctx->start, GF_TRUE);
			}
			ctx->style.style_flags = 0;
			ctx->state = 2;
			if (ctx->end <= ctx->prev_end) {
				GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Overlapping SRT frame %d end "LLD" is at or before previous end "LLD" - removing\n", ctx->curLine, ctx->end, ctx->prev_end));
				ctx->start = ctx->end;
				ctx->state = 3;
			}
			break;

		default:
			/*reset only when text is present*/
			ctx->first_samp = GF_FALSE;

			/*go to line*/
			if (txt_line) {
				gf_isom_text_add_text(ctx->samp, "\n", 1);
				char_len += 1;
			}

			ptr = (char *) szLine;
			{
				size_t _len = gf_utf8_mbstowcs(uniLine, 5000, (const char **) &ptr);
				if (_len == (size_t) -1) {
					GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Invalid UTF data (line %d)\n", ctx->curLine));
					ctx->state = 0;
				}
				len = (u32) _len;
			}
			i=j=0;
			rem_styles = 0;
			rem_color = 0;
			while (i<len) {
				u32 font_style = 0;
				u32 style_nb_chars = 0;
				u32 style_def_type = 0;

				if ( (uniLine[i]=='<') && (uniLine[i+2]=='>')) {
					style_nb_chars = 3;
					style_def_type = 1;
				}
				else if ( (uniLine[i]=='<') && (uniLine[i+1]=='/') && (uniLine[i+3]=='>')) {
					style_def_type = 2;
					style_nb_chars = 4;
				}
				else if (uniLine[i]=='<')  {
					const unsigned short* src = uniLine + i;
					size_t alen = gf_utf8_wcstombs(szLine, 2048, (const unsigned short**) & src);
					szLine[alen] = 0;
					strlwr(szLine);
					if (!strncmp(szLine, "');
								if (e_sep) {
									style_nb_chars = (u32) (1 + e_sep - szLine);
									style_def_type = 1;
								}
							}

						}
					}
					else if (!strncmp(szLine, "", 7) ) {
						style_nb_chars = 7;
						style_def_type = 2;
						font_style = 0xFFFFFFFF;
					}

					else if (!strncmp(szLine, "", 8) ) {
						style_nb_chars = 8;
						style_def_type = 1;
					}
					else if (!strncmp(szLine, "", 9) ) {
						style_nb_chars = 9;
						style_def_type = 2;
						font_style = 0xFFFFFFFF;
					}
					//skip unknown
					else {
						char *a_sep = strstr(szLine, ">");
						if (a_sep) {
							style_nb_chars = (u32) (1 + a_sep - szLine);
							i += style_nb_chars;
							continue;
						}
					}

				}

				/*start of new style*/
				if (style_def_type==1)  {
					/*store prev style*/
					if (set_end_char) {
						assert(set_start_char);
						gf_isom_text_add_style(ctx->samp, &ctx->style);
						set_end_char = set_start_char = GF_FALSE;
						ctx->style.style_flags &= ~rem_styles;
						rem_styles = 0;
						if (rem_color) {
							ctx->style.text_color = ctx->default_color;
							rem_color = 0;
						}
					}
					if (set_start_char && (ctx->style.startCharOffset != j)) {
						ctx->style.endCharOffset = char_len + j;
						if (ctx->style.style_flags) gf_isom_text_add_style(ctx->samp, &ctx->style);
					}
					switch (uniLine[i+1]) {
					case 'b':
					case 'B':
						ctx->style.style_flags |= GF_TXT_STYLE_BOLD;
						set_start_char = GF_TRUE;
						ctx->style.startCharOffset = char_len + j;
						break;
					case 'i':
					case 'I':
						ctx->style.style_flags |= GF_TXT_STYLE_ITALIC;
						set_start_char = GF_TRUE;
						ctx->style.startCharOffset = char_len + j;
						break;
					case 'u':
					case 'U':
						ctx->style.style_flags |= GF_TXT_STYLE_UNDERLINED;
						set_start_char = GF_TRUE;
						ctx->style.startCharOffset = char_len + j;
						break;
					case 's':
					case 'S':
						ctx->style.style_flags |= GF_TXT_STYLE_STRIKETHROUGH;
						set_start_char = GF_TRUE;
						ctx->style.startCharOffset = char_len + j;
						break;
					case 'f':
					case 'F':
						if (font_style) {
							ctx->style.text_color = font_style;
							set_start_char = GF_TRUE;
							ctx->style.startCharOffset = char_len + j;
						}
						break;
					}
					i += style_nb_chars;
					continue;
				}

				/*end of prev style*/
				if (style_def_type==2)  {
					switch (uniLine[i+2]) {
					case 'b':
					case 'B':
						rem_styles |= GF_TXT_STYLE_BOLD;
						set_end_char = GF_TRUE;
						ctx->style.endCharOffset = char_len + j;
						break;
					case 'i':
					case 'I':
						rem_styles |= GF_TXT_STYLE_ITALIC;
						set_end_char = GF_TRUE;
						ctx->style.endCharOffset = char_len + j;
						break;
					case 'u':
					case 'U':
						rem_styles |= GF_TXT_STYLE_UNDERLINED;
						set_end_char = GF_TRUE;
						ctx->style.endCharOffset = char_len + j;
						break;
					case 's':
					case 'S':
						rem_styles |= GF_TXT_STYLE_STRIKETHROUGH;
						set_end_char = GF_TRUE;
						ctx->style.endCharOffset = char_len + j;
						break;
					case 'f':
					case 'F':
						if (font_style) {
							rem_color = 1;
							set_end_char = GF_TRUE;
							ctx->style.endCharOffset = char_len + j;
						}
					}
					i+=style_nb_chars;
					continue;
				}
				/*store style*/
				if (set_end_char) {
					gf_isom_text_add_style(ctx->samp, &ctx->style);
					set_end_char = GF_FALSE;
					set_start_char = GF_TRUE;
					ctx->style.startCharOffset = char_len + j;
					ctx->style.style_flags &= ~rem_styles;
					rem_styles = 0;
					ctx->style.text_color = ctx->default_color;
					rem_color = 0;
				}

				uniText[j] = uniLine[i];
				j++;
				i++;
			}
			/*store last style*/
			if (set_end_char) {
				gf_isom_text_add_style(ctx->samp, &ctx->style);
				set_end_char = GF_FALSE;
				set_start_char = GF_TRUE;
				ctx->style.startCharOffset = char_len + j;
				ctx->style.style_flags &= ~rem_styles;
			}

			char_line = j;
			uniText[j] = 0;

			sptr = (u16 *) uniText;
			len = (u32) gf_utf8_wcstombs(szText, 5000, (const u16 **) &sptr);

			gf_isom_text_add_text(ctx->samp, szText, len);
			char_len += char_line;
			txt_line ++;
			break;
		}

		if (gf_filter_pid_would_block(ctx->opid))
			return GF_OK;
	}

	/*final flush*/
	if (ctx->end && ! ctx->noflush) {
		gf_isom_text_reset(ctx->samp);
		txtin_process_send_text_sample(ctx, ctx->samp, ctx->end, 0, GF_TRUE);
		ctx->end = 0;
	}
	gf_isom_text_reset(ctx->samp);

	return GF_EOS;
}


static Bool txtin_process_event(GF_Filter *filter, const GF_FilterEvent *evt)
{
	GF_TXTIn *ctx = gf_filter_get_udta(filter);
	switch (evt->base.type) {
	case GF_FEVT_PLAY:
		if (ctx->playstate==1) return GF_TRUE;
		ctx->playstate = 1;
		if ((ctx->start_range < 0.1) && (evt->play.start_range<0.1)) return GF_TRUE;
		ctx->start_range = evt->play.start_range;
		ctx->seek_state = 1;
		//cancel play event, we work with full file
		return GF_TRUE;

	case GF_FEVT_STOP:
		ctx->playstate = 2;
		//cancel play event, we work with full file
		return GF_TRUE;
	default:
		return GF_FALSE;
	}
	return GF_FALSE;
}


const GF_FilterRegister *txtin_register(GF_FilterSession *session)
{
	return &TXTInRegister;


static GF_Err gf_text_ttml_setup(GF_Filter *filter, GF_TXTIn *ctx)
{
	GF_Err e;
	u32 i, nb_children, ID;
	u64 file_size;
	s32 sub_fps_num, sub_fps_den;
	GF_XMLAttribute *att;
	GF_XMLNode *root, *node, *body_node;
	const char *lang = ctx->lang;

	ctx->is_setup = GF_TRUE;
	ctx->parser = gf_xml_dom_new();
	e = gf_xml_dom_parse(ctx->parser, ctx->file_name, ttxt_dom_progress, ctx);
	if (e) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] Error parsing TTML file: Line %d - %s. Abort.\n", gf_xml_dom_get_line(ctx->parser), gf_xml_dom_get_error(ctx->parser) ));
		ctx->is_setup = GF_TRUE;
		ctx->non_compliant_ttml = GF_TRUE;
		return e;
	}
	root = gf_xml_dom_get_root(ctx->parser);
	if (!root) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] Error parsing TTML file: no root XML element found. Abort.\n"));
		ctx->non_compliant_ttml = GF_TRUE;
		return GF_NON_COMPLIANT_BITSTREAM;
	}

	/*look for TTML*/
	if (gf_xml_get_element_check_namespace(root, "tt", NULL) != GF_OK) {
		if (root->ns) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("TTML file not recognized: root element is \"%s:%s\" (check your namespaces)\n", root->ns, root->name));
		} else {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("TTML file not recognized: root element is \"%s\"\n", root->name));
		}
		ctx->non_compliant_ttml = GF_TRUE;
		return GF_NOT_SUPPORTED;
	}

	GF_LOG(GF_LOG_DEBUG, GF_LOG_PARSER, ("[TXTIn] TTML EBU-TTD detected\n"));

	root = gf_xml_dom_get_root(ctx->parser);

	/*** root (including language) ***/
	sub_fps_num = 0;
	sub_fps_den = 0;
	i=0;
	while ( (att = (GF_XMLAttribute *)gf_list_enum(root->attributes, &i))) {
		const char *att_name;
		GF_LOG(GF_LOG_DEBUG, GF_LOG_PARSER, ("[TTML] Found root attribute name %s, value %s\n", att->name, att->value));

		att_name = strchr(att->name, ':');
		if (att_name) att_name++;
		else att_name = att->name;

		if (!strcmp(att->name, "xmlns")) {
			if (strcmp(att->value, TTML_NAMESPACE)) {
				GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TTML] XML Namespace %s not recognized, expecting %s\n", att->name, att->value, TTML_NAMESPACE));
				ctx->non_compliant_ttml = GF_TRUE;
				return GF_NON_COMPLIANT_BITSTREAM;
			}
		}
		else if (!strcmp(att->name, "xml:lang") && att->value && strlen(att->value)) {
			lang = att->value;
		}
		else if (!strcmp(att_name, "tickRate") && att->value) {
			ctx->tick_rate = atoi(att->value);
		}
		else if (!strcmp(att_name, "frameRate") && att->value) {
			ctx->ttml_fps_num = atoi(att->value);
			ctx->ttml_fps_den = 1;
		}
		else if (!strcmp(att_name, "frameRateMultiplier") && att->value) {
			char *sep = strchr(att->value, ' ');
			if (!sep) sep = strchr(att->value, '\t');
			if (sep) {
				u8 c = sep[0];
				sep[0] = 0;
				sub_fps_num = atoi(sep);
				sep[0] = c;
				while ((sep[0]==' ') || (sep[0]=='\t'))
					sep++;
				sub_fps_den = atoi(sep);
			}
		}
		else if (!strcmp(att_name, "subFrameRate") && att->value) {
			ctx->ttml_sfps = atoi(att->value);
		}
	}

	if (sub_fps_num && sub_fps_den && ctx->ttml_fps_num) {
		ctx->ttml_fps_num *= sub_fps_num;
		ctx->ttml_fps_den = sub_fps_den;
	}

	//locate body
	nb_children = gf_list_count(root->content);
	body_node = NULL;

	i=0;
	while ( (node = (GF_XMLNode*)gf_list_enum(root->content, &i))) {
		if (node->type) {
			nb_children--;
			continue;
		}
		e = gf_xml_get_element_check_namespace(node, "body", root->ns);
		if (e == GF_BAD_PARAM) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] ignored \"%s\" node, check your namespaces\n", node->name));
		} else if (e == GF_OK) {
			if (body_node) {
				GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TTML EBU-TTD] duplicated \"body\" element. Abort.\n"));
				ctx->non_compliant_ttml = GF_TRUE;
				return GF_NON_COMPLIANT_BITSTREAM;
			}
			body_node = node;
		}
	}
	if (!body_node) {
		GF_LOG(GF_LOG_DEBUG, GF_LOG_PARSER, ("[TTML EBU-TTD] \"body\" element not found, assuming empty doc\n"));
	}

	if (!ctx->div_nodes_list) {
		ctx->div_nodes_list = gf_list_new();
		if (!ctx->div_nodes_list) return GF_OUT_OF_MEM;
	} else {
		gf_list_reset(ctx->div_nodes_list);
	}

	if (body_node) {
		i=0;
		while ( (node = (GF_XMLNode*)gf_list_enum(body_node->content, &i))) {
			if (!node->type) {
				e = gf_xml_get_element_check_namespace(node, "div", root->ns);
				if (e == GF_BAD_PARAM) {
					GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] ignored \"%s\" node, check your namespaces\n", node->name));
				}
			}
			gf_list_add(ctx->div_nodes_list, node);
		}
	}
	file_size = ctx->end;
	if (!ctx->timescale) ctx->timescale = 1000;

	if (!ctx->opid) ctx->opid = gf_filter_pid_new(filter);
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_STREAM_TYPE, &PROP_UINT(GF_STREAM_TEXT) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CODECID, &PROP_UINT(GF_CODECID_SUBS_XML) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_TIMESCALE, &PROP_UINT(ctx->timescale) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DOWN_SIZE, &PROP_LONGUINT(file_size) );

	ID = 1;
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ID, &PROP_UINT(ID) );
	if (ctx->width) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_WIDTH, &PROP_UINT(ctx->width) );
	if (ctx->height) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_HEIGHT, &PROP_UINT(ctx->height) );
	if (ctx->zorder) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ZORDER, &PROP_SINT(ctx->zorder) );
	if (lang) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_LANGUAGE, &PROP_STRING( lang) );
	gf_filter_pid_set_property_str(ctx->opid, "meta:xmlns", &PROP_STRING(TTML_NAMESPACE) );

	/*** body ***/
	ctx->parser_working_copy = gf_xml_dom_new();
	e = gf_xml_dom_parse(ctx->parser_working_copy, ctx->file_name, NULL, NULL);
	assert (e == GF_OK);
	ctx->root_working_copy = gf_xml_dom_get_root(ctx->parser_working_copy);
	assert(ctx->root_working_copy);

	if (body_node) {
		/*remove all the sample entries (instances in body) entries from the working copy, we will add each sample in this clone DOM  to create full XML of each sample*/
		ebu_ttd_remove_samples(ctx->root_working_copy, &ctx->body_node);
		if (!ctx->body_node) {
			return GF_NON_COMPLIANT_BITSTREAM;
		}
	} else {
		ctx->body_node = NULL;
	}

	ctx->current_tt_interval = 0;

	ctx->last_sample_duration = 0;
	ctx->end = 0;
	ctx->first_samp = GF_TRUE;

	txtin_probe_duration(ctx);

	e = ttml_setup_intervals(ctx);
	if (e) return e;

	if (ctx->has_images) {
		char *mime_cfg = "application/ttml+xml;codecs=im1i";
		gf_filter_pid_set_property_str(ctx->opid, "meta:mime", &PROP_STRING(mime_cfg) );
	} else {
		gf_filter_pid_set_property_str(ctx->opid, "meta:mime", NULL);
	}
	return GF_OK;
}


static u32 ttxt_get_color(char *val)
{
	u32 r, g, b, a, res;
	r = g = b = a = 0;
	if (sscanf(val, "%x %x %x %x", &r, &g, &b, &a) != 4) {
		GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Warning: color badly formatted %s\n", val));
	}
	res = (a&0xFF);
	res<<=8;
	res |= (r&0xFF);
	res<<=8;
	res |= (g&0xFF);
	res<<=8;
	res |= (b&0xFF);
	return res;
}


GF_Err txtin_initialize(GF_Filter *filter)
{
	char data[1];
	GF_TXTIn *ctx = gf_filter_get_udta(filter);
	ctx->bs_w = gf_bs_new(data, 1, GF_BITSTREAM_WRITE);

	return GF_OK;
}


static s64 ttml_get_timestamp(GF_TXTIn *ctx, char *value)
{
	u32 h, m, s, ms, f, sf;
	s64 ts = -1;
	u32 len = (u32) strlen(value);

	//tick metrick - cannot be fractional
	if (len && (value[len-1]=='t')) {
		value[len-1] = 0;
		ts = (s64) (atoi(value) * 1000);
		value[len-1] = 't';
		if (ctx->tick_rate)
			ts /= ctx->tick_rate;
	}
	//hours metric, can be fractional
	else if (len && (value[len-1]=='h')) {
		value[len-1] = 0;
		ts = (s64) (atof(value) * 1000 * 3600);
		value[len-1] = 'h';
	}
	//minutes metric, can be fractional
	else if (len && (value[len-1]=='m')) {
		value[len-1] = 0;
		ts = (s64) (atof(value) * 1000 * 60);
		value[len-1] = 'm';
	}
	else if (len && (value[len-1]=='s')) {
		//milliseconds metric, can be fractional but we work at 1ms clock resolution anyway
		if ((len > 1) && (value[len-2]=='m')) {
			value[len-2] = 0;
			ts = (s64) (atof(value));
			value[len-2] = 'm';
		}
		//seconds metric, can be fractional
		else {
			value[len-1] = 0;
			ts = (s64) (atof(value) * 1000);
			value[len-1] = 's';
		}
	}
	//frames metric, can be fractional
	else if (len && (value[len-1]=='f')) {
		f = sf = 0;
		value[len-1] = 0;
		if (sscanf(value, "%u.%u", &f, &sf) != 2) {
			sscanf(value, "%u", &f);
			sf = 0;
		}
		value[len-1] = 'f';

		if (!ctx->ttml_fps_num) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] time indicates frames but no frame rate set, assuming 25 FPS\n"));
			ctx->ttml_fps_num = 25;
			ctx->ttml_fps_den = 1;
		}
		if (sf && !ctx->ttml_sfps) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] time indicates subframes but no subFrameRate set, assuming 1\n"));
			ctx->ttml_sfps = 1;
		}
		ts = ((s64) 1000 * f * ctx->ttml_fps_den) / ctx->ttml_fps_num;
		if (sf)
			ts += ((s64) 1000 * sf * ctx->ttml_fps_den / ctx->ttml_sfps) / ctx->ttml_fps_num;
	}
	else if (sscanf(value, "%u:%u:%u.%u", &h, &m, &s, &ms) == 4) {
		ts = (h*3600 + m*60+s)*1000+ms;
	}
	else if (sscanf(value, "%u:%u:%u:%u.%u", &h, &m, &s, &f, &sf) == 5) {
		ts = (h*3600 + m*60+s)*1000;
		if (!ctx->ttml_fps_num) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] time indicates frames but no frame rate set, assuming 25 FPS\n"));
			ctx->ttml_fps_num = 25;
			ctx->ttml_fps_den = 1;
		}
		if (!ctx->ttml_sfps) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] time indicates subframes but no subFrameRate set, assuming 1\n"));
			ctx->ttml_sfps = 1;
		}
		ts += ((s64) 1000 * f * ctx->ttml_fps_den) / ctx->ttml_fps_num;
		ts += ((s64) 1000 * sf * ctx->ttml_fps_den / ctx->ttml_sfps) / ctx->ttml_fps_num;
	}
	else if (sscanf(value, "%u:%u:%u:%u", &h, &m, &s, &f) == 4) {
		ts = (h*3600 + m*60+s)*1000;
		if (!ctx->ttml_fps_num) {
			GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TTML EBU-TTD] time indicates frames but no frame rate set, assuming 25 FPS\n"));
			ctx->ttml_fps_num = 25;
			ctx->ttml_fps_den = 1;
		}
		ts += ((s64) 1000 * f * ctx->ttml_fps_den) / ctx->ttml_fps_num;
	}
	else if (sscanf(value, "%u:%u:%u", &h, &m, &s) == 3) {
		ts = (h*3600 + m*60+s)*1000;
	}
	return ts;
}


static GF_Err txtin_configure_pid(GF_Filter *filter, GF_FilterPid *pid, Bool is_remove)
{
	GF_Err e;
	const char *src = NULL;
	GF_TXTIn *ctx = gf_filter_get_udta(filter);
	const GF_PropertyValue *prop;

	if (is_remove) {
		ctx->ipid = NULL;
		return GF_OK;
	}

	if (! gf_filter_pid_check_caps(pid))
		return GF_NOT_SUPPORTED;

	//we must have a file path
	prop = gf_filter_pid_get_property(pid, GF_PROP_PID_FILEPATH);
	if (prop && prop->value.string) src = prop->value.string;
	if (!src)
		return GF_NOT_SUPPORTED;

	if (!ctx->ipid) {
		GF_FilterEvent fevt;
		ctx->ipid = pid;

		//we work with full file only, send a play event on source to indicate that
		GF_FEVT_INIT(fevt, GF_FEVT_PLAY, pid);
		fevt.play.start_range = 0;
		fevt.base.on_pid = ctx->ipid;
		fevt.play.full_file_only = GF_TRUE;
		gf_filter_pid_send_event(ctx->ipid, &fevt);
		ctx->file_name = gf_strdup(src);
	} else {
		if (pid != ctx->ipid) {
			return GF_REQUIRES_NEW_INSTANCE;
		}
		if (!strcmp(ctx->file_name, src)) return GF_OK;

		ttxtin_reset(ctx);
		ctx->is_setup = GF_FALSE;
		gf_free(ctx->file_name);
		ctx->file_name = gf_strdup(src);
	}
	//guess type
	e = gf_text_guess_format(ctx->file_name, &ctx->fmt);
	if (e) return e;
	if (!ctx->fmt) {
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTLoad] Unknown text format for %s\n", ctx->file_name));
		return GF_NOT_SUPPORTED;
	}

	if (ctx->webvtt && (ctx->fmt == GF_TXTIN_MODE_SRT))
		ctx->fmt = GF_TXTIN_MODE_WEBVTT;

	switch (ctx->fmt) {
	case GF_TXTIN_MODE_SRT:
		ctx->text_process = txtin_process_srt;
		break;
#ifndef GPAC_DISABLE_VTT
	case GF_TXTIN_MODE_WEBVTT:
		ctx->text_process = txtin_process_webvtt;
		break;
#endif
	case GF_TXTIN_MODE_TTXT:
		ctx->text_process = txtin_process_ttxt;
		break;
	case GF_TXTIN_MODE_TEXML:
		ctx->text_process = txtin_process_texml;
		break;
	case GF_TXTIN_MODE_SUB:
		ctx->text_process = gf_text_process_sub;
		break;
	case GF_TXTIN_MODE_TTML:
		ctx->text_process = gf_text_process_ttml;
		break;
#ifndef GPAC_DISABLE_SWF_IMPORT
	case GF_TXTIN_MODE_SWF_SVG:
		ctx->text_process = gf_text_process_swf;
		break;
#endif
	default:
		return GF_BAD_PARAM;
	}

	return GF_OK;
}


static GF_Err ttml_push_res(GF_TXTIn *ctx, TTMLInterval *interval, u8 *f_data, u32 f_size)
{
	GF_Err e;
	TTMLRes *res;
	GF_List *res_list;
	if (interval) {
		if (!interval->resources) {
			if (ctx->ttml_resources)
				interval->resources = gf_list_clone(ctx->ttml_resources);
			else
				interval->resources = gf_list_new();
		}
		res_list = interval->resources;
	} else {
		if (!ctx->ttml_resources) {
			ctx->ttml_resources = gf_list_new();
		}
		res_list = ctx->ttml_resources;
	}
	if (!res_list) {
		gf_free(f_data);
		return GF_OUT_OF_MEM;
	}
	GF_SAFEALLOC(res, TTMLRes)
	if (!res) {
		gf_free(f_data);
		return GF_OUT_OF_MEM;
	}
	res->size = f_size;
	res->data = f_data;
	if (!interval)
		res->global = GF_TRUE;

	e = gf_list_add(res_list, res);
	if (e) {
		gf_free(res);
		gf_free(f_data);
		return e;
	}
	return GF_OK;
}


static GF_Err txtin_setup_srt(GF_Filter *filter, GF_TXTIn *ctx)
{
	u32 ID, OCR_ES_ID, dsi_len, file_size;
	u8 *dsi;
	GF_TextSampleDescriptor *sd;

	ctx->src = gf_fopen(ctx->file_name, "rb");
	if (!ctx->src) return GF_URL_ERROR;

	file_size = (u32) gf_fsize(ctx->src);

	ctx->unicode_type = gf_text_get_utf_type(ctx->src);
	if (ctx->unicode_type<0) {
		gf_fclose(ctx->src);
		ctx->src = NULL;
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] Unsupported SRT UTF encoding\n"));
		return GF_NOT_SUPPORTED;
	}

	if (!ctx->timescale) ctx->timescale = 1000;
	OCR_ES_ID = ID = 0;

	if (!ctx->opid) ctx->opid = gf_filter_pid_new(filter);
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_STREAM_TYPE, &PROP_UINT(GF_STREAM_TEXT) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CODECID, &PROP_UINT(GF_CODECID_TX3G) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_TIMESCALE, &PROP_UINT(ctx->timescale) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DOWN_SIZE, &PROP_LONGUINT(file_size) );

	if (!ID) ID = 1;
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ID, &PROP_UINT(ID) );
	if (OCR_ES_ID) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CLOCK_ID, &PROP_UINT(OCR_ES_ID) );
	if (ctx->width) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_WIDTH, &PROP_UINT(ctx->width) );
	if (ctx->height) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_HEIGHT, &PROP_UINT(ctx->height) );
	if (ctx->zorder) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ZORDER, &PROP_SINT(ctx->zorder) );
	if (ctx->lang) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_LANGUAGE, &PROP_STRING( ctx->lang) );

	sd = (GF_TextSampleDescriptor*)gf_odf_desc_new(GF_ODF_TX3G_TAG);
	sd->fonts = (GF_FontRecord*)gf_malloc(sizeof(GF_FontRecord));
	sd->font_count = 1;
	sd->fonts[0].fontID = 1;
	sd->fonts[0].fontName = gf_strdup(ctx->fontname ? ctx->fontname : "Serif");
	sd->back_color = 0x00000000;	/*transparent*/
	sd->default_style.fontID = 1;
	sd->default_style.font_size = ctx->fontsize;
	sd->default_style.text_color = 0xFFFFFFFF;	/*white*/
	sd->default_style.style_flags = 0;
	sd->horiz_justif = 1; /*center of scene*/
	sd->vert_justif = (s8) -1;	/*bottom of scene*/

	if (ctx->nodefbox) {
		sd->default_pos.top = sd->default_pos.left = sd->default_pos.right = sd->default_pos.bottom = 0;
	} else if ((sd->default_pos.bottom==sd->default_pos.top) || (sd->default_pos.right==sd->default_pos.left)) {
		sd->default_pos.left = ctx->txtx;
		sd->default_pos.top = ctx->txty;
		sd->default_pos.right = ctx->width + sd->default_pos.left;
		sd->default_pos.bottom = ctx->height + sd->default_pos.top;
	}

	/*store attribs*/
	ctx->style = sd->default_style;
	gf_odf_tx3g_write(sd, &dsi, &dsi_len);
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DECODER_CONFIG, &PROP_DATA_NO_COPY(dsi, dsi_len) );

	gf_odf_desc_del((GF_Descriptor *)sd);

	ctx->default_color = ctx->style.text_color;
	ctx->samp = gf_isom_new_text_sample();
	ctx->state = 0;
	ctx->end = ctx->prev_end = ctx->start = 0;
	ctx->first_samp = GF_TRUE;
	ctx->curLine = 0;

	txtin_probe_duration(ctx);
	return GF_OK;
}


static GF_Err txtin_webvtt_setup(GF_Filter *filter, GF_TXTIn *ctx)
{
	GF_Err e;
	u32 ID, OCR_ES_ID, file_size, w, h;
	Bool is_srt;
	char *ext;

	ctx->src = gf_fopen(ctx->file_name, "rb");
	if (!ctx->src) return GF_URL_ERROR;

	file_size = (u32) gf_fsize(ctx->src);

	ctx->unicode_type = gf_text_get_utf_type(ctx->src);
	if (ctx->unicode_type<0) {
		gf_fclose(ctx->src);
		ctx->src = NULL;
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] Unsupported SRT UTF encoding\n"));
		return GF_NOT_SUPPORTED;
	}
	ext = gf_file_ext_start(ctx->file_name);
	is_srt = (ext && !strnicmp(ext, ".srt", 4)) ? GF_TRUE : GF_FALSE;

	if (!ctx->timescale) ctx->timescale = 1000;
	OCR_ES_ID = ID = 0;

	if (!ctx->opid) ctx->opid = gf_filter_pid_new(filter);
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_STREAM_TYPE, &PROP_UINT(GF_STREAM_TEXT) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CODECID, &PROP_UINT(GF_CODECID_WEBVTT) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_TIMESCALE, &PROP_UINT(ctx->timescale) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DOWN_SIZE, &PROP_LONGUINT(file_size) );

	w = ctx->width;
	h = ctx->height;
	if (!ID) ID = 1;
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ID, &PROP_UINT(ID) );
	if (OCR_ES_ID) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_CLOCK_ID, &PROP_UINT(OCR_ES_ID) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_WIDTH, &PROP_UINT(w) );
	gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_HEIGHT, &PROP_UINT(h) );
	if (ctx->zorder) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_ZORDER, &PROP_SINT(ctx->zorder) );
	if (ctx->lang) gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_LANGUAGE, &PROP_STRING( ctx->lang) );

	ctx->vttparser = gf_webvtt_parser_new();

	e = gf_webvtt_parser_init(ctx->vttparser, ctx->src, ctx->unicode_type, is_srt, ctx, gf_webvtt_import_report, gf_webvtt_flush_sample, gf_webvtt_import_header);
	if (e != GF_OK) {
		gf_webvtt_parser_del(ctx->vttparser);
		ctx->vttparser = NULL;
		GF_LOG(GF_LOG_ERROR, GF_LOG_PARSER, ("[TXTIn] WebVTT parser init error %s\n", gf_error_to_string(e) ));
	}
	//get the header
	e = gf_webvtt_parser_parse(ctx->vttparser);

	txtin_probe_duration(ctx);
	return e;
}


static void txtin_probe_duration(GF_TXTIn *ctx)
{
	GF_Fraction64 dur;
	dur.num = 0;

	if (ctx->fmt == GF_TXTIN_MODE_SWF_SVG) {
#ifndef GPAC_DISABLE_SWF_IMPORT
		u32 frame_count, frame_rate;
		gf_swf_get_duration(ctx->swf_parse, &frame_rate, &frame_count);
		if (frame_count) {
			GF_Fraction64 tdur;
			tdur.num = frame_count;
			tdur.den = frame_rate;
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DURATION, &PROP_FRAC64(tdur));
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_PLAYBACK_MODE, &PROP_UINT(GF_PLAYBACK_MODE_FASTFORWARD ) );
		}
#endif
		return;
	}
	if ((ctx->fmt == GF_TXTIN_MODE_SRT) || (ctx->fmt == GF_TXTIN_MODE_WEBVTT)  || (ctx->fmt == GF_TXTIN_MODE_SUB)) {
		u64 pos = gf_ftell(ctx->src);
		gf_fseek(ctx->src, 0, SEEK_SET);
		while (!gf_feof(ctx->src)) {
			u64 end;
			char szLine[2048];
			char *sOK = gf_text_get_utf8_line(szLine, 2048, ctx->src, ctx->unicode_type);
			if (!sOK) break;
			REM_TRAIL_MARKS(szLine, "\r\n\t ")

			if (ctx->fmt == GF_TXTIN_MODE_SUB) {
				char szText[2048];
				u32 sframe, eframe;
				if (sscanf(szLine, "{%d}{%d}%2047s", &sframe, &eframe, szText) == 3) {
					if (ctx->fps.den)
						end = 1000 * eframe * ctx->fps.num / ctx->fps.den;
					else
						end = 1000 * eframe / 25;
					if (end > (u64) dur.num) dur.num = (s64) end;
				}
			} else {
				u32 eh, em, es, ems;
				char *start = strstr(szLine, "-->");
				if (!start) continue;
				while (start[0] && ((start[0] == ' ') || (start[0] == '\t'))) start++;

				if (sscanf(start, "%u:%u:%u,%u", &eh, &em, &es, &ems) != 4) {
					eh = 0;
					if (sscanf(szLine, "%u:%u,%u", &em, &es, &ems) != 3) {
						continue;
					}
				}
				end = (3600*eh + 60*em + es)*1000 + ems;
				if (end > (u64) dur.num) dur.num = (s64) end;
			}
		}
		gf_fseek(ctx->src, pos, SEEK_SET);
		if (dur.num) {
			dur.den = 1000;
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DURATION, &PROP_FRAC64(dur));
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_PLAYBACK_MODE, &PROP_UINT(GF_PLAYBACK_MODE_FASTFORWARD ) );
		}
		return;
	}
	if ((ctx->fmt == GF_TXTIN_MODE_TTXT) || (ctx->fmt == GF_TXTIN_MODE_TEXML)) {
		u32 i=0;
		GF_XMLNode *node, *root = gf_xml_dom_get_root(ctx->parser);
		while ((node = gf_list_enum(root->content, &i))) {
			u32 j;
			u64 duration;
			GF_XMLAttribute *att;
			if (node->type) {
				continue;
			}
			/*sample text*/
			if ((ctx->fmt == GF_TXTIN_MODE_TTXT) && strcmp(node->name, "TextSample")) continue;
			else if ((ctx->fmt == GF_TXTIN_MODE_TEXML) && strcmp(node->name, "sample")) continue;

			j=0;
			while ( (att=(GF_XMLAttribute*)gf_list_enum(node->attributes, &j))) {
				u32 h, m, s, ms;
				u64 ts=0;
				if (ctx->fmt == GF_TXTIN_MODE_TTXT) {
					if (strcmp(att->name, "sampleTime")) continue;

					if (sscanf(att->value, "%u:%u:%u.%u", &h, &m, &s, &ms) == 4) {
						ts = (h*3600 + m*60 + s)*1000 + ms;
					} else {
						ts = (u32) (atof(att->value) * 1000);
					}
					if (ts > (u64) dur.num) dur.num = (s64) ts;
				} else {
					if (strcmp(att->name, "duration")) continue;
					duration = atoi(att->value);
					dur.num += (s32) ( (1000 * duration) / ctx->txml_timescale);
				}
			}
		}
		if (dur.num) {
			dur.den = 1000;
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DURATION, &PROP_FRAC64(dur));
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_PLAYBACK_MODE, &PROP_UINT(GF_PLAYBACK_MODE_FASTFORWARD ) );
		}
		return;
	}

	if (ctx->fmt == GF_TXTIN_MODE_TTML) {
		u32 i=0, k=0;
		GF_XMLNode *div_node;

		while ((div_node = gf_list_enum(ctx->div_nodes_list, &k))) {
			GF_XMLNode *node;
			while ((node = gf_list_enum(div_node->content, &i))) {
				GF_XMLNode *p_node;
				GF_XMLAttribute *att;
				u32 h, m, s, ms, p_idx=0;
				u64 ts_end=0;
				h = m = s = ms = 0;
				while ( (att = (GF_XMLAttribute*)gf_list_enum(node->attributes, &p_idx))) {
					if (strcmp(att->name, "end")) continue;

					if (sscanf(att->value, "%u:%u:%u.%u", &h, &m, &s, &ms) == 4) {
						ts_end = (h*3600 + m*60+s)*1000+ms;
					} else if (sscanf(att->value, "%u:%u:%u", &h, &m, &s) == 3) {
						ts_end = (h*3600 + m*60+s)*1000;
					}
				}
				//or under a <span>
				p_idx = 0;
				while ( (p_node = (GF_XMLNode*)gf_list_enum(node->content, &p_idx))) {
					u32 span_idx = 0;
					while ( (att = (GF_XMLAttribute*)gf_list_enum(p_node->attributes, &span_idx))) {
						if (strcmp(att->name, "end")) continue;
						if (sscanf(att->value, "%u:%u:%u.%u", &h, &m, &s, &ms) == 4) {
							ts_end = (h*3600 + m*60+s)*1000+ms;
						} else if (sscanf(att->value, "%u:%u:%u", &h, &m, &s) == 3) {
							ts_end = (h*3600 + m*60+s)*1000;
						}
					}
				}
				if (ts_end > (u64) dur.num) dur.num = (s64) ts_end;
			}
		}
		if (dur.num) {
			dur.den = 1000;
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_DURATION, &PROP_FRAC64(dur));
			gf_filter_pid_set_property(ctx->opid, GF_PROP_PID_PLAYBACK_MODE, &PROP_UINT(GF_PLAYBACK_MODE_FASTFORWARD ) );
		}
		return;
	}
	GF_LOG(GF_LOG_WARNING, GF_LOG_PARSER, ("[TXTIn] Duration probing not supported for format %d\n", ctx->fmt));
}