# Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. import os.path as op from gzip import GzipFile from io import SEEK_SET, BytesIO import numpy as np from scipy.sparse import issparse from ..utils import _file_like, logger, verbose, warn from .constants import FIFF from .tag import ( Tag, _call_dict_names, _matrix_info, _read_tag_header, read_tag, ) from .tree import dir_tree_find, make_dir_tree class _NoCloseRead: """Create a wrapper that will not close when used as a context manager.""" def __init__(self, fid): self.fid = fid def __enter__(self): return self.fid def __exit__(self, type_, value, traceback): return def close(self): return def seek(self, offset, whence=SEEK_SET): return self.fid.seek(offset, whence) def read(self, size=-1): return self.fid.read(size) def _fiff_get_fid(fname): """Open a FIF file with no additional parsing.""" if _file_like(fname): fid = _NoCloseRead(fname) fid.seek(0) else: fname = str(fname) if op.splitext(fname)[1].lower() == ".gz": logger.debug("Using gzip") fid = GzipFile(fname, "rb") # Open in binary mode else: logger.debug("Using normal I/O") fid = open(fname, "rb") # Open in binary mode return fid def _get_next_fname(fid, fname, tree): """Get the next filename in split files.""" nodes_list = dir_tree_find(tree, FIFF.FIFFB_REF) next_fname = None for nodes in nodes_list: next_fname = None for ent in nodes["directory"]: if ent.kind == FIFF.FIFF_REF_ROLE: tag = read_tag(fid, ent.pos) role = int(tag.data.item()) if role != FIFF.FIFFV_ROLE_NEXT_FILE: next_fname = None break if ent.kind == FIFF.FIFF_REF_FILE_NAME: tag = read_tag(fid, ent.pos) next_fname = op.join(op.dirname(fname), tag.data) if ent.kind == FIFF.FIFF_REF_FILE_NUM: # Some files don't have the name, just the number. So # we construct the name from the current name. if next_fname is not None: continue next_num = read_tag(fid, ent.pos).data.item() path, base = op.split(fname) idx = base.find(".") idx2 = base.rfind("-") num_str = base[idx2 + 1 : idx] if not num_str.isdigit(): idx2 = -1 if idx2 < 0 and next_num == 1: # this is the first file, which may not be numbered next_fname = op.join( path, f"{base[:idx]}-{next_num:d}.{base[idx + 1 :]}", ) continue next_fname = op.join( path, f"{base[:idx2]}-{next_num:d}.{base[idx + 1 :]}" ) if next_fname is not None: break return next_fname @verbose def fiff_open(fname, preload=False, verbose=None): """Open a FIF file. Parameters ---------- fname : path-like | fid Name of the fif file, or an opened file (will seek back to 0). preload : bool If True, all data from the file is read into a memory buffer. This requires more memory, but can be faster for I/O operations that require frequent seeks. %(verbose)s Returns ------- fid : file The file descriptor of the open file. tree : fif tree The tree is a complex structure filled with dictionaries, lists and tags. directory : list A list of tags. """ fid = _fiff_get_fid(fname) try: return _fiff_open(fname, fid, preload) except Exception: fid.close() raise def _fiff_open(fname, fid, preload): # do preloading of entire file if preload: # note that StringIO objects instantiated this way are read-only, # but that's okay here since we are using mode "rb" anyway with fid as fid_old: fid = BytesIO(fid_old.read()) tag = _read_tag_header(fid, 0) # Check that this looks like a fif file prefix = f"file {repr(fname)} does not" if tag.kind != FIFF.FIFF_FILE_ID: raise ValueError(f"{prefix} start with a file id tag") if tag.type != FIFF.FIFFT_ID_STRUCT: raise ValueError(f"{prefix} start with a file id tag") if tag.size != 20: raise ValueError(f"{prefix} start with a file id tag") tag = read_tag(fid, tag.next_pos) if tag.kind != FIFF.FIFF_DIR_POINTER: raise ValueError(f"{prefix} have a directory pointer") # Read or create the directory tree logger.debug(f" Creating tag directory for {fname}...") dirpos = int(tag.data.item()) read_slow = True if dirpos > 0: dir_tag = read_tag(fid, dirpos) if dir_tag is None or dir_tag.data is None: fid.seek(0, 2) # move to end of file size = fid.tell() extra = "" if size > dirpos else f" > file size {size}" warn( "FIF tag directory missing at the end of the file " f"(at byte {dirpos}{extra}), possibly corrupted file: {fname}" ) else: directory = dir_tag.data read_slow = False if read_slow: pos = 0 fid.seek(pos, 0) directory = list() while pos is not None: tag = _read_tag_header(fid, pos) if tag is None: break # HACK : to fix file ending with empty tag... pos = tag.next_pos directory.append(tag) tree, _ = make_dir_tree(fid, directory) logger.debug("[done]") # Back to the beginning fid.seek(0) return fid, tree, directory @verbose def show_fiff( fname, indent=" ", read_limit=np.inf, max_str=30, output=str, tag=None, *, show_bytes=False, verbose=None, ): """Show FIFF information. This function is similar to mne_show_fiff. Parameters ---------- fname : path-like Filename to evaluate. indent : str How to indent the lines. read_limit : int Max number of bytes of data to read from a tag. Can be np.inf to always read all data (helps test read completion). max_str : int Max number of characters of string representation to print for each tag's data. output : type Either str or list. str is a convenience output for printing. tag : int | None Provide information about this tag. If None (default), all information is shown. show_bytes : bool If True (default False), print the byte offsets of each tag. %(verbose)s Returns ------- contents : str The contents of the file. """ if output not in [list, str]: raise ValueError("output must be list or str") if isinstance(tag, str): # command mne show_fiff passes string tag = int(tag) f, tree, directory = fiff_open(fname) # This gets set to 0 (unknown) by fiff_open, but FIFFB_ROOT probably # makes more sense for display tree["block"] = FIFF.FIFFB_ROOT with f as fid: out = _show_tree( fid, tree, indent=indent, level=0, read_limit=read_limit, max_str=max_str, tag_id=tag, show_bytes=show_bytes, ) if output is str: out = "\n".join(out) return out def _find_type(value, fmts=("FIFF_",), exclude=("FIFF_UNIT",)): """Find matching values.""" value = int(value) vals = [ k for k, v in FIFF.items() if v == value and any(fmt in k for fmt in fmts) and not any(exc in k for exc in exclude) ] if len(vals) == 0: vals = ["???"] return vals def _show_tree( fid, tree, indent, level, read_limit, max_str, tag_id, *, show_bytes=False, ): """Show FIFF tree.""" this_idt = indent * level next_idt = indent * (level + 1) # print block-level information found_types = "/".join(_find_type(tree["block"], fmts=["FIFFB_"])) out = [f"{this_idt}{str(int(tree['block'])).ljust(4)} = {found_types}"] tag_found = False if tag_id is None or out[0].strip().startswith(str(tag_id)): tag_found = True if tree["directory"] is not None: kinds = [ent.kind for ent in tree["directory"]] + [-1] types = [ent.type for ent in tree["directory"]] sizes = [ent.size for ent in tree["directory"]] poss = [ent.pos for ent in tree["directory"]] counter = 0 good = True for k, kn, size, pos, type_ in zip(kinds[:-1], kinds[1:], sizes, poss, types): if not tag_found and k != tag_id: continue tag = Tag(kind=k, type=type_, size=size, next=FIFF.FIFFV_NEXT_NONE, pos=pos) if read_limit is None or size <= read_limit: try: tag = read_tag(fid, pos) except Exception: good = False if kn == k: # don't print if the next item is the same type (count 'em) counter += 1 else: if show_bytes: at = f" @{pos}" else: at = "" # find the tag type this_type = _find_type(k, fmts=["FIFF_"]) # prepend a count if necessary prepend = "x" + str(counter + 1) + ": " if counter > 0 else "" postpend = "" # print tag data nicely if tag.data is not None: postpend = " = " + str(tag.data)[:max_str] if isinstance(tag.data, np.ndarray): if tag.data.size > 1: postpend += " ... array size=" + str(tag.data.size) elif isinstance(tag.data, dict): postpend += " ... dict len=" + str(len(tag.data)) elif isinstance(tag.data, str): postpend += " ... str len=" + str(len(tag.data)) elif isinstance(tag.data, (list, tuple)): postpend += " ... list len=" + str(len(tag.data)) elif issparse(tag.data): postpend += ( f" ... sparse ({tag.data.getformat()}) shape=" f"{tag.data.shape}" ) else: postpend += " ... type=" + str(type(tag.data)) postpend = ">" * 20 + f"BAD @{pos}" if not good else postpend matrix_info = _matrix_info(tag) if matrix_info is not None: _, type_, _, _ = matrix_info type_ = _call_dict_names.get(type_, f"?{type_}?") this_type = "/".join(this_type) out += [ f"{next_idt}{prepend}{str(k).ljust(4)} = " f"{this_type}{at} ({size}b {type_}) {postpend}" ] out[-1] = out[-1].replace("\n", "ΒΆ") counter = 0 good = True if tag_id in kinds: tag_found = True if not tag_found: out = [""] level = -1 # removes extra indent # deal with children for branch in tree["children"]: out += _show_tree( fid, branch, indent, level + 1, read_limit, max_str, tag_id, show_bytes=show_bytes, ) return out